diff --git a/README.md b/README.md index 289b765aaf..15a87a915c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -# ExoPlayer # +# ExoPlayer Readme # -ExoPlayer is an application level media player for Android. It provides an alternative to Android’s -MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports -features not currently supported by Android’s MediaPlayer API, including DASH and SmoothStreaming -adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can -be updated through Play Store application updates. +## Description ## + +ExoPlayer is an application level media player for Android. It provides an +alternative to Android’s MediaPlayer API for playing audio and video both +locally and over the Internet. ExoPlayer supports features not currently +supported by Android’s MediaPlayer API, including DASH and SmoothStreaming +adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to +customize and extend, and can be updated through Play Store application +updates. ## News ## @@ -14,7 +18,8 @@ Read news, hints and tips on the [news][] page. ## Documentation ## -* The [developer guide][] provides a wealth of information to help you get started. +* The [developer guide][] provides a wealth of information to help you get +started. * The [class reference][] documents the ExoPlayer library classes. * The [release notes][] document the major changes in each release. @@ -22,31 +27,37 @@ Read news, hints and tips on the [news][] page. [class reference]: https://google.github.io/ExoPlayer/doc/reference [release notes]: https://github.com/google/ExoPlayer/blob/dev/RELEASENOTES.md -## Using ExoPlayer ## +## Project branches ## -#### Via jCenter #### + * The [master][] branch holds the most recent minor release. + * Most development work happens on the [dev][] branch. + * Additional development branches may be established for major features. -The easiest way to get started using ExoPlayer is by including the following in your project's -`build.gradle` file: +[master]: https://github.com/google/ExoPlayer/tree/master +[dev]: https://github.com/google/ExoPlayer/tree/dev + +## Using Eclipse ## + +The repository includes Eclipse projects for both the ExoPlayer library and its +accompanying demo application. To get started: + + 1. Install Eclipse and setup the [Android SDK][]. + + 1. Open Eclipse and navigate to File->Import->General->Existing Projects into + Workspace. + + 1. Select the root directory of the repository. + + 1. Import the ExoPlayerDemo and ExoPlayerLib projects. + +[Android SDK]: http://developer.android.com/sdk/index.html + + +## Using Gradle ## + +ExoPlayer can also be built using Gradle. You can include it as a dependent project and build from source: ``` -gradle -compile 'com.google.android.exoplayer:exoplayer:rX.X.X' -``` - -where `rX.X.X` is the your preferred version. For the latest version, see the project's -[Releases][]. For more details, see the project on [Bintray][]. - -[Releases]: https://github.com/google/ExoPlayer/releases -[Bintray]: https://bintray.com/google/exoplayer/exoplayer/view - -#### As source #### - -ExoPlayer can also be built from source using Gradle. You can include it as a dependent project like -so: - -``` -gradle // settings.gradle include ':app', ':..:ExoPlayer:library' @@ -56,40 +67,18 @@ dependencies { } ``` -#### As a jar #### - If you want to use ExoPlayer as a jar, run: ``` -sh ./gradlew jarRelease ``` -and copy `library.jar` to the libs folder of your new project. +and copy library.jar to the libs-folder of your new project. -## Developing ExoPlayer ## +The project is also available on [jCenter](https://bintray.com/google/exoplayer/exoplayer/view): -#### Project branches #### +``` +compile 'com.google.android.exoplayer:exoplayer:rX.X.X' +``` - * The [`master`][master] branch holds the most recent minor release. - * Most development work happens on the [`dev`][dev] branch. - * Additional development branches may be established for major features. - -[master]: https://github.com/google/ExoPlayer/tree/master -[dev]: https://github.com/google/ExoPlayer/tree/dev - -#### Using Android Studio #### - -To develop ExoPlayer using Android Studio, simply open the ExoPlayer project in the root directory -of the repository. - -#### Using Eclipse #### - -To develop ExoPlayer using Eclipse: - - 1. Install Eclipse and setup the [Android SDK][]. - 1. Open Eclipse and navigate to File->Import->General->Existing Projects into Workspace. - 1. Select the root directory of the repository. - 1. Import the projects. - -[Android SDK]: http://developer.android.com/sdk/index.html +Where `rX.X.X` should be replaced with the desired version. diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/DefaultExtractorInputTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/DefaultExtractorInputTest.java index 1d7ac16c38..fb2d91654d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/DefaultExtractorInputTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/DefaultExtractorInputTest.java @@ -157,11 +157,39 @@ public class DefaultExtractorInputTest extends TestCase { assertEquals(0, input.getPosition()); } + public void testReadFullyHalfPeeked() throws IOException, InterruptedException { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(4); + + input.readFully(target, 0, TEST_DATA.length); + + // Check the read data is correct. + assertTrue(Arrays.equals(TEST_DATA, target)); + assertEquals(TEST_DATA.length, input.getPosition()); + } + + public void testSkip() throws IOException, InterruptedException { + FakeDataSource testDataSource = buildDataSource(); + DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNBOUNDED); + // We expect to perform three skips of three bytes, as setup in buildTestDataSource. + for (int i = 0; i < 3; i++) { + assertEquals(3, input.skip(TEST_DATA.length)); + } + // Check we're now indicated that the end of input is reached. + int expectedEndOfInput = input.skip(TEST_DATA.length); + assertEquals(-1, expectedEndOfInput); + } + public void testSkipFullyOnce() throws IOException, InterruptedException { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(TEST_DATA.length); assertEquals(TEST_DATA.length, input.getPosition()); + // Check that we see end of input if we skip again with allowEndOfInput set. + boolean result = input.skipFully(1, true); + assertFalse(result); // Check that we fail with EOFException we skip again. try { input.skipFully(1); @@ -180,6 +208,20 @@ public class DefaultExtractorInputTest extends TestCase { assertEquals(5 + 4, input.getPosition()); } + public void testSkipFullyTwicePeeked() throws IOException, InterruptedException { + // Skip TEST_DATA. + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length); + + int halfLength = TEST_DATA.length / 2; + input.skipFully(halfLength); + assertEquals(halfLength, input.getPosition()); + + input.skipFully(TEST_DATA.length - halfLength); + assertEquals(TEST_DATA.length, input.getPosition()); + } + public void testSkipFullyTooMuch() throws IOException, InterruptedException { // Skip more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -190,6 +232,17 @@ public class DefaultExtractorInputTest extends TestCase { // Expected. } assertEquals(0, input.getPosition()); + + // Skip more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because + // the end of input isn't encountered immediately. Position should not update. + input = createDefaultExtractorInput(); + try { + input.skipFully(TEST_DATA.length + 1, true); + fail(); + } catch (EOFException e) { + // Expected. + } + assertEquals(0, input.getPosition()); } public void testSkipFullyWithFailingDataSource() throws IOException, InterruptedException { @@ -225,6 +278,29 @@ public class DefaultExtractorInputTest extends TestCase { } } + public void testPeekFully() throws IOException, InterruptedException { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + input.peekFully(target, 0, TEST_DATA.length); + + // Check that we read the whole of TEST_DATA. + assertTrue(Arrays.equals(TEST_DATA, target)); + assertEquals(0, input.getPosition()); + + // Check that we can read again from the buffer + byte[] target2 = new byte[TEST_DATA.length]; + input.readFully(target2, 0, TEST_DATA.length); + assertTrue(Arrays.equals(TEST_DATA, target2)); + + // Check that we fail with EOFException if we peek again + try { + input.peekFully(target, 0, 1); + fail(); + } catch (EOFException e) { + // Expected. + } + } + private static FakeDataSource buildDataSource() throws IOException { FakeDataSource.Builder builder = new FakeDataSource.Builder(); builder.appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 3)); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java index ba66ff9eab..1a1a52cbec 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java @@ -51,58 +51,13 @@ public final class DefaultExtractorInput implements ExtractorInput { @Override public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - - int totalBytesRead = 0; - - if (peekBufferLength > 0) { - int peekBytes = Math.min(peekBufferLength, length); - System.arraycopy(peekBuffer, 0, target, offset, peekBytes); - updatePeekBuffer(peekBytes); - totalBytesRead += peekBytes; - } - - if (totalBytesRead != length) { - int readBytes = dataSource.read(target, offset + totalBytesRead, length - totalBytesRead); - if (readBytes == C.RESULT_END_OF_INPUT) { - if (totalBytesRead == 0) { - return C.RESULT_END_OF_INPUT; - } - } else { - totalBytesRead += readBytes; - } - } - - position += totalBytesRead; - return totalBytesRead; + return internalRead(target, offset, length, true, false, false); } @Override public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { - int peekBytes = Math.min(peekBufferLength, length); - System.arraycopy(peekBuffer, 0, target, offset, peekBytes); - offset += peekBytes; - int remaining = length - peekBytes; - while (remaining > 0) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - int bytesRead = dataSource.read(target, offset, remaining); - if (bytesRead == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput && remaining == length) { - return false; - } - throw new EOFException(); - } - offset += bytesRead; - remaining -= bytesRead; - } - updatePeekBuffer(peekBytes); - position += length; - return true; + return internalReadFully(target, offset, length, allowEndOfInput, false); } @Override @@ -111,71 +66,39 @@ public final class DefaultExtractorInput implements ExtractorInput { readFully(target, offset, length, false); } + @Override + public int skip(int length) throws IOException, InterruptedException { + return internalRead(null, 0, Math.min(SCRATCH_SPACE.length + peekBufferLength, length), true, + true, false); + } + + @Override + public boolean skipFully(final int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return internalReadFully(null, 0, length, allowEndOfInput, true); + } + @Override public void skipFully(int length) throws IOException, InterruptedException { - int peekBytes = Math.min(peekBufferLength, length); - int remaining = length - peekBytes; - while (remaining > 0) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - int bytesRead = dataSource.read(SCRATCH_SPACE, 0, Math.min(SCRATCH_SPACE.length, remaining)); - if (bytesRead == C.RESULT_END_OF_INPUT) { - throw new EOFException(); - } - remaining -= bytesRead; - } - updatePeekBuffer(peekBytes); - position += length; + skipFully(length, false); } @Override public void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException { - ensureSpaceForPeek(length); - int peekBytes = Math.min(peekBufferLength - peekBufferPosition, length); - System.arraycopy(peekBuffer, peekBufferPosition, target, offset, peekBytes); - offset += peekBytes; - int fillBytes = length - peekBytes; - int remaining = fillBytes; - int writePosition = peekBufferLength; - while (remaining > 0) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - int bytesRead = dataSource.read(peekBuffer, writePosition, remaining); - if (bytesRead == C.RESULT_END_OF_INPUT) { - throw new EOFException(); - } - System.arraycopy(peekBuffer, writePosition, target, offset, bytesRead); - remaining -= bytesRead; - writePosition += bytesRead; - offset += bytesRead; - } - peekBufferPosition += length; - peekBufferLength += fillBytes; + advancePeekPosition(length); + System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length); } @Override public void advancePeekPosition(int length) throws IOException, InterruptedException { ensureSpaceForPeek(length); - int peekBytes = Math.min(peekBufferLength - peekBufferPosition, length); - int fillBytes = length - peekBytes; - int remaining = fillBytes; - int writePosition = peekBufferLength; - while (remaining > 0) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - int bytesRead = dataSource.read(peekBuffer, writePosition, remaining); - if (bytesRead == C.RESULT_END_OF_INPUT) { - throw new EOFException(); - } - remaining -= bytesRead; - writePosition += bytesRead; - } peekBufferPosition += length; - peekBufferLength += fillBytes; + if (peekBufferPosition > peekBufferLength) { + readFromDataSource(peekBuffer, peekBufferLength, peekBufferPosition - peekBufferLength, + false, false, 0, true); + peekBufferLength = peekBufferPosition; + } } @Override @@ -215,4 +138,101 @@ public final class DefaultExtractorInput implements ExtractorInput { System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); } + /** + * Internal read method. + * + * @see #internalRead(byte[], int, int, boolean, boolean, boolean) + */ + private boolean internalReadFully(byte[] target, int offset, int length, boolean allowEndOfInput, + boolean skip) throws InterruptedException, IOException { + return internalRead(target, offset, length, allowEndOfInput, skip, true) + != C.RESULT_END_OF_INPUT; + } + + /** + * Internal read method. + * + * @see #readFromPeekBuffer(byte[], int, int, boolean) + * @see #readFromDataSource(byte[], int, int, boolean, boolean, int, boolean) + */ + private int internalRead(byte[] target, int offset, int length, boolean allowEndOfInput, + boolean skip, boolean readFully) throws InterruptedException, IOException { + int totalBytesRead = readFromPeekBuffer(target, offset, length, skip); + totalBytesRead = readFromDataSource(target, offset, length, allowEndOfInput, skip, + totalBytesRead, readFully); + if (totalBytesRead != C.RESULT_END_OF_INPUT) { + position += totalBytesRead; + } + return totalBytesRead; + } + + /** + * Reads from the peek buffer + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @param skip If true, instead of reading skip the data + * @return The number of bytes read + */ + private int readFromPeekBuffer(byte[] target, int offset, int length, boolean skip) { + if (peekBufferLength == 0) { + return 0; + } + + int peekBytes = Math.min(peekBufferLength, length); + if (!skip) { + System.arraycopy(peekBuffer, 0, target, offset, peekBytes); + } + updatePeekBuffer(peekBytes); + return peekBytes; + } + + /** + * Reads from the data source + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @param returnEOIifNoReadBytes If true on encountering the end of the input having read no data + * should result in {@link C#RESULT_END_OF_INPUT} being returned. + * @param skip If true, instead of reading skip the data + * @param totalBytesRead Number of bytes read until now for the external request + * @param readFully If true reads the requested {@code length} in full. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int readFromDataSource(byte[] target, int offset, int length, + boolean returnEOIifNoReadBytes, boolean skip, int totalBytesRead, boolean readFully) + throws InterruptedException, IOException { + while (totalBytesRead < length) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + int bytesRead = !skip + ? dataSource.read(target, offset + totalBytesRead, length - totalBytesRead) + : dataSource.read(SCRATCH_SPACE, 0, + Math.min(SCRATCH_SPACE.length, length - totalBytesRead)); + + if (bytesRead == C.RESULT_END_OF_INPUT) { + if (returnEOIifNoReadBytes && totalBytesRead == 0) { + return C.RESULT_END_OF_INPUT; + } + if (readFully) { + throw new EOFException(); + } + } else { + totalBytesRead += bytesRead; + } + + if (!readFully) { + break; + } + } + return totalBytesRead; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java index bf5788fc8c..f99503d232 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java @@ -79,6 +79,33 @@ public interface ExtractorInput { */ void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + /** + * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read. + * + * @param length The maximum number of bytes to skip from the input. + * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int skip(int length) throws IOException, InterruptedException; + + /** + * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read. + * + * @param length The number of bytes to skip from the input. + * @param allowEndOfInput True if encountering the end of the input having skipped no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. + * @return True if the skip was successful. False if the end of the input was encountered having + * skipped no data. + * @throws EOFException If the end of input was encountered having partially satisfied the skip + * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were + * skipped and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException; + /** * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read. *