Further H264/H265 code deduping + fix NAL unescaping.

This commit is contained in:
Oliver Woodman 2015-06-05 20:04:56 +01:00
parent ae466cc59b
commit 24e897d39f
4 changed files with 107 additions and 111 deletions

View file

@ -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.
* <p>
* 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.
*/

View file

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

View file

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

View file

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