Skip to content

Commit

Permalink
Improve support for Ogg truncated content
Browse files Browse the repository at this point in the history
Issue:#7608
PiperOrigin-RevId: 382081687
  • Loading branch information
kim-vde authored and icbaker committed Jul 16, 2021
1 parent ff5694a commit e272e3b
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 47 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
is set incorrectly
([#4083](https://github.com/google/ExoPlayer/issues/4083)). Such content
is malformed and should be re-encoded.
* Improve support for truncated Ogg streams
([#7608](https://github.com/google/ExoPlayer/issues/7608)).
* HLS:
* Fix issue where playback of a live event could become stuck rather than
transitioning to `STATE_ENDED` when the event ends
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
package com.google.android.exoplayer2.extractor;

import com.google.android.exoplayer2.C;
import java.io.EOFException;
import java.io.IOException;

/** Extractor related utility methods. */
/* package */ final class ExtractorUtil {
public final class ExtractorUtil {

/**
* Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the
Expand Down Expand Up @@ -47,5 +48,62 @@ public static int peekToLength(ExtractorInput input, byte[] target, int offset,
return totalBytesPeeked;
}

/**
* Equivalent to {@link ExtractorInput#readFully(byte[], int, int)} except that it returns {@code
* false} instead of throwing an {@link EOFException} if the end of input is encountered without
* having fully satisfied the read.
*/
public static boolean readFullyQuietly(
ExtractorInput input, byte[] output, int offset, int length) throws IOException {
try {
input.readFully(output, offset, length);
} catch (EOFException e) {
return false;
}
return true;
}

/**
* Equivalent to {@link ExtractorInput#skipFully(int)} except that it returns {@code false}
* instead of throwing an {@link EOFException} if the end of input is encountered without having
* fully satisfied the skip.
*/
public static boolean skipFullyQuietly(ExtractorInput input, int length) throws IOException {
try {
input.skipFully(length);
} catch (EOFException e) {
return false;
}
return true;
}

/**
* Peeks data from {@code input}, respecting {@code allowEndOfInput}. Returns true if the peek is
* successful.
*
* <p>If {@code allowEndOfInput=false} then encountering the end of the input (whether before or
* after reading some data) will throw {@link EOFException}.
*
* <p>If {@code allowEndOfInput=true} then encountering the end of the input (even after reading
* some data) will return {@code false}.
*
* <p>This is slightly different to the behaviour of {@link ExtractorInput#peekFully(byte[], int,
* int, boolean)}, where {@code allowEndOfInput=true} only returns false (and suppresses the
* exception) if the end of the input is reached before reading any data.
*/
public static boolean peekFullyQuietly(
ExtractorInput input, byte[] output, int offset, int length, boolean allowEndOfInput)
throws IOException {
try {
return input.peekFully(output, offset, length, /* allowEndOfInput= */ allowEndOfInput);
} catch (EOFException e) {
if (allowEndOfInput) {
return false;
} else {
throw e;
}
}
}

private ExtractorUtil() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ogg;

import static com.google.android.exoplayer2.extractor.ExtractorUtil.skipFullyQuietly;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
Expand Down Expand Up @@ -229,13 +231,21 @@ long readGranuleOfLastPage(ExtractorInput input) throws IOException {
if (!pageHeader.skipToNextPage(input)) {
throw new EOFException();
}
do {
pageHeader.populate(input, /* quiet= */ false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
} while ((pageHeader.type & 0x04) != 0x04
pageHeader.populate(input, /* quiet= */ false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
long granulePosition = pageHeader.granulePosition;
while ((pageHeader.type & 0x04) != 0x04
&& pageHeader.skipToNextPage(input)
&& input.getPosition() < payloadEndPosition);
return pageHeader.granulePosition;
&& input.getPosition() < payloadEndPosition) {
boolean hasPopulated = pageHeader.populate(input, /* quiet= */ true);
if (!hasPopulated || !skipFullyQuietly(input, pageHeader.headerSize + pageHeader.bodySize)) {
// The input file contains a partial page at the end. Ignore it and return the granule
// position of the last complete page.
return granulePosition;
}
granulePosition = pageHeader.granulePosition;
}
return granulePosition;
}

private final class OggSeekMap implements SeekMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ogg;

import static com.google.android.exoplayer2.extractor.ExtractorUtil.readFullyQuietly;
import static com.google.android.exoplayer2.extractor.ExtractorUtil.skipFullyQuietly;
import static java.lang.Math.max;

import com.google.android.exoplayer2.C;
Expand Down Expand Up @@ -55,7 +57,7 @@ public void reset() {
*
* @param input The {@link ExtractorInput} to read data from.
* @return {@code true} if the read was successful. The read fails if the end of the input is
* encountered without reading data.
* encountered without reading the whole packet.
* @throws IOException If reading from the input fails.
*/
public boolean populate(ExtractorInput input) throws IOException {
Expand All @@ -80,15 +82,19 @@ public boolean populate(ExtractorInput input) throws IOException {
bytesToSkip += calculatePacketSize(segmentIndex);
segmentIndex += segmentCount;
}
input.skipFully(bytesToSkip);
if (!skipFullyQuietly(input, bytesToSkip)) {
return false;
}
currentSegmentIndex = segmentIndex;
}

int size = calculatePacketSize(currentSegmentIndex);
int segmentIndex = currentSegmentIndex + segmentCount;
if (size > 0) {
packetArray.ensureCapacity(packetArray.limit() + size);
input.readFully(packetArray.getData(), packetArray.limit(), size);
if (!readFullyQuietly(input, packetArray.getData(), packetArray.limit(), size)) {
return false;
}
packetArray.setLimit(packetArray.limit() + size);
populated = pageHeader.laces[segmentIndex - 1] != 255;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
*/
package com.google.android.exoplayer2.extractor.ogg;

import static com.google.android.exoplayer2.extractor.ExtractorUtil.peekFullyQuietly;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;

/**
Expand Down Expand Up @@ -106,7 +107,8 @@ public boolean skipToNextPage(ExtractorInput input, long limit) throws IOExcepti
Assertions.checkArgument(input.getPosition() == input.getPeekPosition());
scratch.reset(/* limit= */ CAPTURE_PATTERN_SIZE);
while ((limit == C.POSITION_UNSET || input.getPosition() + CAPTURE_PATTERN_SIZE < limit)
&& peekSafely(input, scratch.getData(), 0, CAPTURE_PATTERN_SIZE, /* quiet= */ true)) {
&& peekFullyQuietly(
input, scratch.getData(), 0, CAPTURE_PATTERN_SIZE, /* allowEndOfInput= */ true)) {
scratch.setPosition(0);
if (scratch.readUnsignedInt() == CAPTURE_PATTERN) {
input.resetPeekPosition();
Expand All @@ -127,14 +129,13 @@ && peekSafely(input, scratch.getData(), 0, CAPTURE_PATTERN_SIZE, /* quiet= */ tr
* @param input The {@link ExtractorInput} to read from.
* @param quiet Whether to return {@code false} rather than throwing an exception if the header
* cannot be populated.
* @return Whether the read was successful. The read fails if the end of the input is encountered
* without reading data.
* @return Whether the header was entirely populated.
* @throws IOException If reading data fails or the stream is invalid.
*/
public boolean populate(ExtractorInput input, boolean quiet) throws IOException {
reset();
scratch.reset(/* limit= */ EMPTY_PAGE_HEADER_SIZE);
if (!peekSafely(input, scratch.getData(), 0, EMPTY_PAGE_HEADER_SIZE, quiet)
if (!peekFullyQuietly(input, scratch.getData(), 0, EMPTY_PAGE_HEADER_SIZE, quiet)
|| scratch.readUnsignedInt() != CAPTURE_PATTERN) {
return false;
}
Expand All @@ -158,39 +159,14 @@ public boolean populate(ExtractorInput input, boolean quiet) throws IOException

// calculate total size of header including laces
scratch.reset(/* limit= */ pageSegmentCount);
input.peekFully(scratch.getData(), 0, pageSegmentCount);
if (!peekFullyQuietly(input, scratch.getData(), 0, pageSegmentCount, quiet)) {
return false;
}
for (int i = 0; i < pageSegmentCount; i++) {
laces[i] = scratch.readUnsignedByte();
bodySize += laces[i];
}

return true;
}

/**
* Peek data from {@code input}, respecting {@code quiet}. Return true if the peek is successful.
*
* <p>If {@code quiet=false} then encountering the end of the input (whether before or after
* reading some data) will throw {@link EOFException}.
*
* <p>If {@code quiet=true} then encountering the end of the input (even after reading some data)
* will return {@code false}.
*
* <p>This is slightly different to the behaviour of {@link ExtractorInput#peekFully(byte[], int,
* int, boolean)}, where {@code allowEndOfInput=true} only returns false (and suppresses the
* exception) if the end of the input is reached before reading any data.
*/
private static boolean peekSafely(
ExtractorInput input, byte[] output, int offset, int length, boolean quiet)
throws IOException {
try {
return input.peekFully(output, offset, length, /* allowEndOfInput= */ quiet);
} catch (EOFException e) {
if (quiet) {
return false;
} else {
throw e;
}
}
}
}
Loading

0 comments on commit e272e3b

Please sign in to comment.