diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java index d246bd6d8c..776adb5bc9 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java @@ -66,7 +66,6 @@ import java.util.List; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - private int[] scratchEscapePositions; public H264Reader(TrackOutput output, SeiReader seiReader, boolean idrKeyframesOnly) { super(output); @@ -77,7 +76,6 @@ import java.util.List; pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); seiWrapper = new ParsableByteArray(); - scratchEscapePositions = new int[10]; } @Override @@ -191,7 +189,7 @@ import java.util.List; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (sei.endNalUnit(discardPadding)) { - int unescapedLength = unescapeStream(sei.nalData, sei.nalLength); + int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength); seiWrapper.reset(sei.nalData, unescapedLength); seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. seiReader.consume(seiWrapper, pesTimeUs, true); @@ -208,7 +206,7 @@ import java.util.List; initializationData.add(ppsData); // Unescape and then parse the SPS unit. - unescapeStream(sps.nalData, sps.nalLength); + NalUnitUtil.unescapeStream(sps.nalData, sps.nalLength); ParsableBitArray bitArray = new ParsableBitArray(sps.nalData); bitArray.skipBits(32); // NAL header int profileIdc = bitArray.readBits(8); @@ -322,57 +320,6 @@ import java.util.List; } } - /** - * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with - * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length. - *

- * See ISO/IEC 14496-10:2005(E) page 36 for more information. - * - * @param data The data to unescape. - * @param limit The limit (exclusive) of the data to unescape. - * @return The length of the unescaped data. - */ - private int unescapeStream(byte[] data, int limit) { - int position = 0; - int scratchEscapeCount = 0; - while (position < limit) { - position = findNextUnescapeIndex(data, position, limit); - if (position < limit) { - if (scratchEscapePositions.length <= scratchEscapeCount) { - // Grow scratchEscapePositions to hold a larger number of positions. - scratchEscapePositions = Arrays.copyOf(scratchEscapePositions, - scratchEscapePositions.length * 2); - } - scratchEscapePositions[scratchEscapeCount++] = position; - position += 3; - } - } - - int unescapedLength = limit - scratchEscapeCount; - int escapedPosition = 0; // The position being read from. - int unescapedPosition = 0; // The position being written to. - for (int i = 0; i < scratchEscapeCount; i++) { - int nextEscapePosition = scratchEscapePositions[i]; - int copyLength = nextEscapePosition - escapedPosition; - System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength); - escapedPosition += copyLength + 3; - unescapedPosition += copyLength + 2; - } - - int remainingLength = unescapedLength - unescapedPosition; - System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength); - return unescapedLength; - } - - private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { - for (int i = offset; i < limit - 2; i++) { - if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { - return i; - } - } - return limit; - } - /** * A buffer specifically for IFR units that can be used to parse the IFR's slice type. */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java index 6170ad48a8..d8bc483284 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer.util.ParsableByteArray; import android.util.Log; -import java.util.Arrays; import java.util.Collections; /** @@ -71,7 +70,6 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - private int[] scratchEscapePositions; public H265Reader(TrackOutput output, SeiReader seiReader) { super(output); @@ -83,7 +81,6 @@ import java.util.Collections; prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128); suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128); seiWrapper = new ParsableByteArray(); - scratchEscapePositions = new int[10]; } @Override @@ -189,7 +186,7 @@ import java.util.Collections; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (prefixSei.endNalUnit(discardPadding)) { - int unescapedLength = unescapeStream(prefixSei.nalData, prefixSei.nalLength); + int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength); seiWrapper.reset(prefixSei.nalData, unescapedLength); // Skip the NAL prefix and type. @@ -197,7 +194,7 @@ import java.util.Collections; seiReader.consume(seiWrapper, pesTimeUs, true); } if (suffixSei.endNalUnit(discardPadding)) { - int unescapedLength = unescapeStream(suffixSei.nalData, suffixSei.nalLength); + int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength); seiWrapper.reset(suffixSei.nalData, unescapedLength); // Skip the NAL prefix and type. @@ -215,7 +212,7 @@ import java.util.Collections; System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength); // Unescape and then parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1. - unescapeStream(sps.nalData, sps.nalLength); + NalUnitUtil.unescapeStream(sps.nalData, sps.nalLength); ParsableBitArray bitArray = new ParsableBitArray(sps.nalData); bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id int maxSubLayersMinus1 = bitArray.readBits(3); @@ -339,56 +336,6 @@ import java.util.Collections; } } - // TODO: Deduplicate with H264Reader. - /** - * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with - * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length. - * - * @param data The data to unescape. - * @param limit The limit (exclusive) of the data to unescape. - * @return The length of the unescaped data. - */ - private int unescapeStream(byte[] data, int limit) { - int position = 0; - int scratchEscapeCount = 0; - while (position < limit) { - position = findNextUnescapeIndex(data, position, limit); - if (position < limit) { - if (scratchEscapePositions.length <= scratchEscapeCount) { - // Grow scratchEscapePositions to hold a larger number of positions. - scratchEscapePositions = Arrays.copyOf(scratchEscapePositions, - scratchEscapePositions.length * 2); - } - scratchEscapePositions[scratchEscapeCount++] = position; - position += 3; - } - } - - int unescapedLength = limit - scratchEscapeCount; - int escapedPosition = 0; // The position being read from. - int unescapedPosition = 0; // The position being written to. - for (int i = 0; i < scratchEscapeCount; i++) { - int nextEscapePosition = scratchEscapePositions[i]; - int copyLength = nextEscapePosition - escapedPosition; - System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength); - escapedPosition += copyLength + 3; - unescapedPosition += copyLength + 2; - } - - int remainingLength = unescapedLength - unescapedPosition; - System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength); - return unescapedLength; - } - - private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { - for (int i = offset; i < limit - 2; i++) { - if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { - return i; - } - } - return limit; - } - /** Returns whether the NAL unit is a random access point. */ private static boolean isRandomAccessPoint(int nalUnitType) { return nalUnitType == BLA_W_LP || nalUnitType == BLA_W_RADL || nalUnitType == BLA_N_LP diff --git a/library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java index 6d658a750b..93575ba80f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.util; import java.nio.ByteBuffer; +import java.util.Arrays; /** * Utility methods for handling H.264/AVC and H.265/HEVC NAL units. @@ -48,6 +49,61 @@ public final class NalUnitUtil { 2f }; + private static final Object scratchEscapePositionsLock = new Object(); + + /** + * Temporary store for positions of escape codes in {@link #unescapeStream(byte[], int)}. Guarded + * by {@link #scratchEscapePositionsLock}. + */ + private static int[] scratchEscapePositions = new int[10]; + + /** + * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with + * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length. + *

+ * Executions of this method are mutually exclusive, so it should not be called with very large + * buffers. + * + * @param data The data to unescape. + * @param limit The limit (exclusive) of the data to unescape. + * @return The length of the unescaped data. + */ + public static int unescapeStream(byte[] data, int limit) { + synchronized (scratchEscapePositionsLock) { + int position = 0; + int scratchEscapeCount = 0; + while (position < limit) { + position = findNextUnescapeIndex(data, position, limit); + if (position < limit) { + if (scratchEscapePositions.length <= scratchEscapeCount) { + // Grow scratchEscapePositions to hold a larger number of positions. + scratchEscapePositions = Arrays.copyOf(scratchEscapePositions, + scratchEscapePositions.length * 2); + } + scratchEscapePositions[scratchEscapeCount++] = position; + position += 3; + } + } + + int unescapedLength = limit - scratchEscapeCount; + int escapedPosition = 0; // The position being read from. + int unescapedPosition = 0; // The position being written to. + for (int i = 0; i < scratchEscapeCount; i++) { + int nextEscapePosition = scratchEscapePositions[i]; + int copyLength = nextEscapePosition - escapedPosition; + System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength); + unescapedPosition += copyLength; + data[unescapedPosition++] = 0; + data[unescapedPosition++] = 0; + escapedPosition += copyLength + 3; + } + + int remainingLength = unescapedLength - unescapedPosition; + System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength); + return unescapedLength; + } + } + /** * Replaces length prefixes of NAL units in {@code buffer} with start code prefixes, within the * {@code size} bytes preceding the buffer's position. @@ -189,6 +245,15 @@ public final class NalUnitUtil { prefixFlags[2] = false; } + private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + for (int i = offset; i < limit - 2; i++) { + if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { + return i; + } + } + return limit; + } + /** * Reads an unsigned integer into an integer. This method is suitable for use when it can be * assumed that the top bit will always be set to zero. diff --git a/library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java b/library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java index ecd97cbff0..0a10c1794c 100644 --- a/library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java +++ b/library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java @@ -110,6 +110,18 @@ public class NalUnitUtilTest extends TestCase { assertPrefixFlagsCleared(prefixFlags); } + public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { + assertUnescapeDoesNotModify(""); + assertUnescapeDoesNotModify("0000"); + assertUnescapeDoesNotModify("172BF38A3C"); + assertUnescapeDoesNotModify("000004"); + } + + public void testUnescapeModifiesBuffersWithStartCodes() { + assertUnescapeMatchesExpected("00000301", "000001"); + assertUnescapeMatchesExpected("0000030200000300", "000002000000"); + } + private static byte[] buildTestData() { byte[] data = new byte[20]; for (int i = 0; i < data.length; i++) { @@ -130,4 +142,29 @@ public class NalUnitUtilTest extends TestCase { assertEquals(false, flags[0] || flags[1] || flags[2]); } + private static void assertUnescapeDoesNotModify(String input) { + assertUnescapeMatchesExpected(input, input); + } + + private static void assertUnescapeMatchesExpected(String input, String expectedOutput) { + byte[] bitstream = getByteArrayForHexString(input); + byte[] expectedOutputBitstream = getByteArrayForHexString(expectedOutput); + int count = NalUnitUtil.unescapeStream(bitstream, bitstream.length); + assertEquals(expectedOutputBitstream.length, count); + byte[] outputBitstream = new byte[count]; + System.arraycopy(bitstream, 0, outputBitstream, 0, count); + assertTrue(Arrays.equals(expectedOutputBitstream, outputBitstream)); + } + + private static byte[] getByteArrayForHexString(String hexString) { + int length = hexString.length(); + Assertions.checkArgument(length % 2 == 0); + byte[] result = new byte[length / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = (byte) ((Character.digit(hexString.charAt(i * 2), 16) << 4) + + Character.digit(hexString.charAt(i * 2 + 1), 16)); + } + return result; + } + }