diff --git a/library/src/androidTest/java/com/google/android/exoplayer/util/NalUnitUtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer/util/NalUnitUtilTest.java index 9bd1d07d3d..ef40ea10ad 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/util/NalUnitUtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/util/NalUnitUtilTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.util; +import static com.google.android.exoplayer.testutil.TestUtil.createByteArray; + import junit.framework.TestCase; import java.nio.ByteBuffer; @@ -27,6 +29,10 @@ public class NalUnitUtilTest extends TestCase { private static final int TEST_PARTIAL_NAL_POSITION = 4; private static final int TEST_NAL_POSITION = 10; + private static final byte[] SPS_TEST_DATA = createByteArray(0x00, 0x00, 0x01, 0x67, 0x4D, 0x40, + 0x16, 0xEC, 0xA0, 0x50, 0x17, 0xFC, 0xB8, 0x08, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, + 0x00, 0x0F, 0x47, 0x8B, 0x16, 0xCB); + private static final int SPS_TEST_DATA_OFFSET = 3; public void testFindNalUnit() { byte[] data = buildTestData(); @@ -111,6 +117,21 @@ public class NalUnitUtilTest extends TestCase { assertPrefixFlagsCleared(prefixFlags); } + public void testParseSpsNalUnit() { + NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, + SPS_TEST_DATA.length); + assertEquals(640, data.width); + assertEquals(360, data.height); + assertFalse(data.deltaPicOrderAlwaysZeroFlag); + assertTrue(data.frameMbsOnlyFlag); + assertEquals(4, data.frameNumLength); + assertEquals(6, data.picOrderCntLsbLength); + assertEquals(0, data.seqParameterSetId); + assertEquals(1.0f, data.pixelWidthAspectRatio); + assertEquals(0, data.picOrderCountType); + assertFalse(data.separateColorPlaneFlag); + } + public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { assertUnescapeDoesNotModify(""); assertUnescapeDoesNotModify("0000"); diff --git a/library/src/androidTest/java/com/google/android/exoplayer/util/ParsableNalUnitBitArrayTest.java b/library/src/androidTest/java/com/google/android/exoplayer/util/ParsableNalUnitBitArrayTest.java new file mode 100644 index 0000000000..66394c6063 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/util/ParsableNalUnitBitArrayTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 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.exoplayer.util; + +import static com.google.android.exoplayer.testutil.TestUtil.createByteArray; + +import junit.framework.TestCase; + +/** + * Tests for {@link ParsableNalUnitBitArray}. + */ +public final class ParsableNalUnitBitArrayTest extends TestCase { + + private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0); + private static final byte[] ALL_ESCAPING_TEST_DATA = createByteArray(0, 0, 3, 0, 0, 3, 0, 0, 3); + private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127); + + public void testReadNoEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length); + assertEquals(0x000300, array.readBits(24)); + assertEquals(0, array.readBits(7)); + assertTrue(array.readBit()); + assertEquals(0x030000, array.readBits(24)); + assertFalse(array.canReadBits(1)); + assertFalse(array.canReadBits(8)); + } + + public void testReadNoEscapingTruncated() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4); + assertTrue(array.canReadBits(32)); + array.skipBits(32); + assertFalse(array.canReadBits(1)); + try { + array.readBit(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + public void testReadAllEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); + assertTrue(array.canReadBits(48)); + assertFalse(array.canReadBits(49)); + assertEquals(0, array.readBits(15)); + assertFalse(array.readBit()); + assertEquals(0, array.readBits(17)); + assertEquals(0, array.readBits(15)); + } + + public void testReadMix() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length); + assertTrue(array.canReadBits(56)); + assertFalse(array.canReadBits(57)); + assertEquals(127, array.readBits(7)); + assertEquals(2, array.readBits(2)); + assertEquals(3, array.readBits(17)); + assertEquals(126, array.readBits(7)); + assertEquals(127, array.readBits(23)); + assertFalse(array.canReadBits(1)); + } + + public void testReadExpGolomb() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); + assertTrue(array.canReadExpGolombCodedNum()); + assertEquals(0, array.readUnsignedExpGolombCodedInt()); + assertEquals(6, array.readUnsignedExpGolombCodedInt()); + assertEquals(0, array.readUnsignedExpGolombCodedInt()); + assertFalse(array.canReadExpGolombCodedNum()); + try { + array.readUnsignedExpGolombCodedInt(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + public void testReadExpGolombWithEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); + assertFalse(array.canReadExpGolombCodedNum()); + array.skipBits(1); + assertTrue(array.canReadExpGolombCodedNum()); + assertEquals(32767, array.readUnsignedExpGolombCodedInt()); + assertFalse(array.canReadBits(1)); + } + + public void testReset() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2); + assertFalse(array.canReadExpGolombCodedNum()); + assertTrue(array.canReadBits(16)); + assertFalse(array.canReadBits(17)); + array.reset(createByteArray(0, 0, 3, 0), 0, 4); + assertTrue(array.canReadBits(24)); + assertFalse(array.canReadBits(25)); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java index 70bcc29337..363d8c1457 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.NalUnitUtil; -import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; import java.util.ArrayList; @@ -160,14 +159,12 @@ import java.util.List; int width = Format.NO_VALUE; int height = Format.NO_VALUE; if (numSequenceParameterSets > 0) { - // Parse the first sequence parameter set to obtain pixelWidthAspectRatio. - ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0)); - // Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte). - spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1)); - NalUnitUtil.SpsData sps = NalUnitUtil.parseSpsNalUnit(spsDataBitArray); - width = sps.width; - height = sps.height; - pixelWidthAspectRatio = sps.pixelWidthAspectRatio; + byte[] sps = initializationData.get(0); + NalUnitUtil.SpsData spsData = + NalUnitUtil.parseSpsNalUnit(sps, nalUnitLengthFieldLength, sps.length); + width = spsData.width; + height = spsData.height; + pixelWidthAspectRatio = spsData.pixelWidthAspectRatio; } return new AvcSequenceHeaderData(initializationData, nalUnitLengthFieldLength, diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 9e5a81a367..b533861fe0 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.NalUnitUtil; -import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; @@ -666,10 +665,9 @@ import java.util.List; if (numSequenceParameterSets > 0) { // Parse the first sequence parameter set to obtain pixelWidthAspectRatio. - ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0)); - // Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte). - spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1)); - pixelWidthAspectRatio = NalUnitUtil.parseSpsNalUnit(spsDataBitArray).pixelWidthAspectRatio; + byte[] sps = initializationData.get(0); + pixelWidthAspectRatio = NalUnitUtil + .parseSpsNalUnit(sps, nalUnitLengthFieldLength, sps.length).pixelWidthAspectRatio; } return new AvcCData(initializationData, nalUnitLengthFieldLength, pixelWidthAspectRatio); 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 2627f3cceb..09a176f068 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 @@ -21,8 +21,8 @@ import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.NalUnitUtil; import com.google.android.exoplayer.util.NalUnitUtil.SpsData; -import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.ParsableNalUnitBitArray; import android.util.SparseArray; @@ -169,8 +169,8 @@ import java.util.List; List initializationData = new ArrayList<>(); initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength)); initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); - NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(unescape(sps)); - NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(unescape(pps)); + NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); + NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio)); @@ -181,11 +181,11 @@ import java.util.List; pps.reset(); } } else if (sps.isCompleted()) { - NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(unescape(sps)); + NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); sampleReader.putSps(spsData); sps.reset(); } else if (pps.isCompleted()) { - NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(unescape(pps)); + NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); sampleReader.putPps(ppsData); pps.reset(); } @@ -199,13 +199,6 @@ import java.util.List; sampleReader.endNalUnit(position, offset); } - private static ParsableBitArray unescape(NalUnitTargetBuffer buffer) { - int length = NalUnitUtil.unescapeStream(buffer.nalData, buffer.nalLength); - ParsableBitArray bitArray = new ParsableBitArray(buffer.nalData, length); - bitArray.skipBits(32); // NAL header - return bitArray; - } - /** * Consumes a stream of NAL units and outputs samples. */ @@ -221,9 +214,9 @@ import java.util.List; private final TrackOutput output; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; - private final ParsableBitArray scratch; private final SparseArray sps; private final SparseArray pps; + private final ParsableNalUnitBitArray bitArray; private byte[] buffer; private int bufferLength; @@ -251,8 +244,8 @@ import java.util.List; pps = new SparseArray<>(); previousSliceHeader = new SliceHeaderData(); sliceHeader = new SliceHeaderData(); - scratch = new ParsableBitArray(); buffer = new byte[DEFAULT_BUFFER_SIZE]; + bitArray = new ParsableNalUnitBitArray(buffer, 0, 0); reset(); } @@ -310,34 +303,34 @@ import java.util.List; System.arraycopy(data, offset, buffer, bufferLength, readLength); bufferLength += readLength; - scratch.reset(buffer, bufferLength); - if (scratch.bitsLeft() < 8) { + bitArray.reset(buffer, 0, bufferLength); + if (!bitArray.canReadBits(8)) { return; } - scratch.skipBits(1); // forbidden_zero_bit - int nalRefIdc = scratch.readBits(2); - scratch.skipBits(5); // nal_unit_type + bitArray.skipBits(1); // forbidden_zero_bit + int nalRefIdc = bitArray.readBits(2); + bitArray.skipBits(5); // nal_unit_type // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013) // subsection 7.3.3. - if (!scratch.canReadExpGolombCodedNum()) { + if (!bitArray.canReadExpGolombCodedNum()) { return; } - scratch.readUnsignedExpGolombCodedInt(); // first_mb_in_slice - if (!scratch.canReadExpGolombCodedNum()) { + bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_slice + if (!bitArray.canReadExpGolombCodedNum()) { return; } - int sliceType = scratch.readUnsignedExpGolombCodedInt(); + int sliceType = bitArray.readUnsignedExpGolombCodedInt(); if (!detectAccessUnits) { // There are AUDs in the stream so the rest of the header can be ignored. isFilling = false; sliceHeader.setSliceType(sliceType); return; } - if (!scratch.canReadExpGolombCodedNum()) { + if (!bitArray.canReadExpGolombCodedNum()) { return; } - int picParameterSetId = scratch.readUnsignedExpGolombCodedInt(); + int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt(); if (pps.indexOfKey(picParameterSetId) < 0) { // We have not seen the PPS yet, so don't try to parse the slice header. isFilling = false; @@ -346,65 +339,65 @@ import java.util.List; NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId); NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId); if (spsData.separateColorPlaneFlag) { - if (scratch.bitsLeft() < 2) { + if (!bitArray.canReadBits(2)) { return; } - scratch.skipBits(2); // colour_plane_id + bitArray.skipBits(2); // colour_plane_id } - if (scratch.bitsLeft() < spsData.frameNumLength) { + if (!bitArray.canReadBits(spsData.frameNumLength)) { return; } boolean fieldPicFlag = false; boolean bottomFieldFlagPresent = false; boolean bottomFieldFlag = false; - int frameNum = scratch.readBits(spsData.frameNumLength); + int frameNum = bitArray.readBits(spsData.frameNumLength); if (!spsData.frameMbsOnlyFlag) { - if (scratch.bitsLeft() < 1) { + if (!bitArray.canReadBits(1)) { return; } - fieldPicFlag = scratch.readBit(); + fieldPicFlag = bitArray.readBit(); if (fieldPicFlag) { - if (scratch.bitsLeft() < 1) { + if (!bitArray.canReadBits(1)) { return; } - bottomFieldFlag = scratch.readBit(); + bottomFieldFlag = bitArray.readBit(); bottomFieldFlagPresent = true; } } boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR; int idrPicId = 0; if (idrPicFlag) { - if (!scratch.canReadExpGolombCodedNum()) { + if (!bitArray.canReadExpGolombCodedNum()) { return; } - idrPicId = scratch.readUnsignedExpGolombCodedInt(); + idrPicId = bitArray.readUnsignedExpGolombCodedInt(); } int picOrderCntLsb = 0; int deltaPicOrderCntBottom = 0; int deltaPicOrderCnt0 = 0; int deltaPicOrderCnt1 = 0; if (spsData.picOrderCountType == 0) { - if (scratch.bitsLeft() < spsData.picOrderCntLsbLength) { + if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) { return; } - picOrderCntLsb = scratch.readBits(spsData.picOrderCntLsbLength); + picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength); if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) { - if (!scratch.canReadExpGolombCodedNum()) { + if (!bitArray.canReadExpGolombCodedNum()) { return; } - deltaPicOrderCntBottom = scratch.readSignedExpGolombCodedInt(); + deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt(); } } else if (spsData.picOrderCountType == 1 && !spsData.deltaPicOrderAlwaysZeroFlag) { - if (!scratch.canReadExpGolombCodedNum()) { + if (!bitArray.canReadExpGolombCodedNum()) { return; } - deltaPicOrderCnt0 = scratch.readSignedExpGolombCodedInt(); + deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt(); if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) { - if (!scratch.canReadExpGolombCodedNum()) { + if (!bitArray.canReadExpGolombCodedNum()) { return; } - deltaPicOrderCnt1 = scratch.readSignedExpGolombCodedInt(); + deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt(); } } sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag, 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 7bfc4edaa9..b838325548 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 @@ -20,8 +20,8 @@ import com.google.android.exoplayer.Format; import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.NalUnitUtil; -import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.ParsableNalUnitBitArray; import android.util.Log; @@ -210,9 +210,8 @@ import java.util.Collections; System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength); 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. - NalUnitUtil.unescapeStream(sps.nalData, sps.nalLength); - ParsableBitArray bitArray = new ParsableBitArray(sps.nalData); + // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1. + ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength); bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id int maxSubLayersMinus1 = bitArray.readBits(3); bitArray.skipBits(1); // sps_temporal_id_nesting_flag @@ -316,7 +315,7 @@ import java.util.Collections; /** * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4. */ - private static void skipScalingList(ParsableBitArray bitArray) { + private static void skipScalingList(ParsableNalUnitBitArray bitArray) { for (int sizeId = 0; sizeId < 4; sizeId++) { for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) { if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId] @@ -340,7 +339,7 @@ import java.util.Collections; * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of * them. See H.265/HEVC (2014) 7.3.7. */ - private static void skipShortTermRefPicSets(ParsableBitArray bitArray) { + private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) { int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt(); boolean interRefPicSetPredictionFlag = false; int numNegativePics = 0; 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 00a9f27ade..8c99b98cc6 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 @@ -236,11 +236,14 @@ public final class NalUnitUtil { * Parses an SPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection * 7.3.2.1.1. * - * @param data A {@link ParsableBitArray} containing the SPS data. The position must to set to the - * start of the data (i.e. the first bit of the profile_idc field). + * @param nalData A buffer containing escaped SPS data. + * @param nalOffset The offset of the NAL unit header in {@code nalData}. + * @param nalLimit The limit of the NAL unit in {@code nalData}. * @return A parsed representation of the SPS data. */ - public static SpsData parseSpsNalUnit(ParsableBitArray data) { + public static SpsData parseSpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) { + ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit); + data.skipBits(8); // nal_unit int profileIdc = data.readBits(8); data.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); @@ -348,11 +351,14 @@ public final class NalUnitUtil { * Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection * 7.3.2.2. * - * @param data A {@link ParsableBitArray} containing the PPS data. The position must to set to the - * start of the data (i.e. the first bit of the pic_parameter_set_id field). + * @param nalData A buffer containing escaped PPS data. + * @param nalOffset The offset of the NAL unit header in {@code nalData}. + * @param nalLimit The limit of the NAL unit in {@code nalData}. * @return A parsed representation of the PPS data. */ - public static PpsData parsePpsNalUnit(ParsableBitArray data) { + public static PpsData parsePpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) { + ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit); + data.skipBits(8); // nal_unit int picParameterSetId = data.readUnsignedExpGolombCodedInt(); int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); data.skipBits(1); // entropy_coding_mode_flag @@ -459,7 +465,7 @@ public final class NalUnitUtil { return limit; } - private static void skipScalingList(ParsableBitArray bitArray, int size) { + private static void skipScalingList(ParsableNalUnitBitArray bitArray, int size) { int lastScale = 8; int nextScale = 8; for (int i = 0; i < size; i++) { diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java index 443bfee406..031ee23cf4 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java @@ -177,52 +177,6 @@ public final class ParsableBitArray { return returnValue; } - /** - * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current - * offset. The offset is not modified. - * - * @return Whether it is possible to read an Exp-Golomb-coded integer. - */ - public boolean canReadExpGolombCodedNum() { - int initialByteOffset = byteOffset; - int initialBitOffset = bitOffset; - int leadingZeros = 0; - while (byteOffset < byteLimit && !readBit()) { - leadingZeros++; - } - boolean hitLimit = byteOffset == byteLimit; - byteOffset = initialByteOffset; - bitOffset = initialBitOffset; - return !hitLimit && bitsLeft() >= leadingZeros * 2 + 1; - } - - /** - * Reads an unsigned Exp-Golomb-coded format integer. - * - * @return The value of the parsed Exp-Golomb-coded integer. - */ - public int readUnsignedExpGolombCodedInt() { - return readExpGolombCodeNum(); - } - - /** - * Reads an signed Exp-Golomb-coded format integer. - * - * @return The value of the parsed Exp-Golomb-coded integer. - */ - public int readSignedExpGolombCodedInt() { - int codeNum = readExpGolombCodeNum(); - return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); - } - - private int readExpGolombCodeNum() { - int leadingZeros = 0; - while (!readBit()) { - leadingZeros++; - } - return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); - } - private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableNalUnitBitArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableNalUnitBitArray.java new file mode 100644 index 0000000000..5333e9f6f4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableNalUnitBitArray.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 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.exoplayer.util; + +/** + * Wraps a byte array, providing methods that allow it to be read as a NAL unit bitstream. + *

+ * Whenever the byte sequence [0, 0, 3] appears in the wrapped byte array, it is treated as [0, 0] + * for all reading/skipping operations, which makes the bitstream appear to be unescaped. + */ +public final class ParsableNalUnitBitArray { + + private byte[] data; + private int byteLimit; + + // The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3]. + private int byteOffset; + private int bitOffset; + + /** + * @param data The data to wrap. + * @param offset The byte offset in {@code data} to start reading from. + * @param limit The byte offset of the end of the bitstream in {@code data}. + */ + public ParsableNalUnitBitArray(byte[] data, int offset, int limit) { + reset(data, offset, limit); + } + + /** + * Resets the wrapped data, limit and offset. + * + * @param data The data to wrap. + * @param offset The byte offset in {@code data} to start reading from. + * @param limit The byte offset of the end of the bitstream in {@code data}. + */ + public void reset(byte[] data, int offset, int limit) { + this.data = data; + byteOffset = offset; + byteLimit = limit; + bitOffset = 0; + assertValidOffset(); + } + + /** + * Skips bits and moves current reading position forward. + * + * @param n The number of bits to skip. + */ + public void skipBits(int n) { + int oldByteOffset = byteOffset; + byteOffset += (n / 8); + bitOffset += (n % 8); + if (bitOffset > 7) { + byteOffset++; + bitOffset -= 8; + } + for (int i = oldByteOffset + 1; i <= byteOffset; i++) { + if (shouldSkipByte(i)) { + // Skip the byte and move forward to check three bytes ahead. + byteOffset++; + i += 2; + } + } + assertValidOffset(); + } + + /** + * Returns whether it is possible to read {@code n} bits starting from the current offset. The + * offset is not modified. + * + * @return Whether it is possible to read {@code n} bits. + */ + public boolean canReadBits(int n) { + int oldByteOffset = byteOffset; + int newByteOffset = byteOffset + (n / 8); + int newBitOffset = bitOffset + (n % 8); + if (newBitOffset > 7) { + newByteOffset++; + newBitOffset -= 8; + } + for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset < byteLimit; i++) { + if (shouldSkipByte(i)) { + // Skip the byte and move forward to check three bytes ahead. + newByteOffset++; + i += 2; + } + } + return newByteOffset < byteLimit || (newByteOffset == byteLimit && newBitOffset == 0); + } + + /** + * Reads a single bit. + * + * @return True if the bit is set. False otherwise. + */ + public boolean readBit() { + return readBits(1) == 1; + } + + /** + * Reads up to 32 bits. + * + * @param numBits The number of bits to read. + * @return An integer whose bottom n bits hold the read data. + */ + public int readBits(int numBits) { + if (numBits == 0) { + return 0; + } + + int returnValue = 0; + + // Read as many whole bytes as we can. + int wholeBytes = (numBits / 8); + for (int i = 0; i < wholeBytes; i++) { + int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1; + int byteValue; + if (bitOffset != 0) { + byteValue = ((data[byteOffset] & 0xFF) << bitOffset) + | ((data[nextByteOffset] & 0xFF) >>> (8 - bitOffset)); + } else { + byteValue = data[byteOffset]; + } + numBits -= 8; + returnValue |= (byteValue & 0xFF) << numBits; + byteOffset = nextByteOffset; + } + + // Read any remaining bits. + if (numBits > 0) { + int nextBit = bitOffset + numBits; + byte writeMask = (byte) (0xFF >> (8 - numBits)); + int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1; + + if (nextBit > 8) { + // Combine bits from current byte and next byte. + returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) + | ((data[nextByteOffset] & 0xFF) >> (16 - nextBit))) & writeMask)); + byteOffset = nextByteOffset; + } else { + // Bits to be read only within current byte. + returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); + if (nextBit == 8) { + byteOffset = nextByteOffset; + } + } + + bitOffset = nextBit % 8; + } + + assertValidOffset(); + return returnValue; + } + + /** + * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current + * offset. The offset is not modified. + * + * @return Whether it is possible to read an Exp-Golomb-coded integer. + */ + public boolean canReadExpGolombCodedNum() { + int initialByteOffset = byteOffset; + int initialBitOffset = bitOffset; + int leadingZeros = 0; + while (byteOffset < byteLimit && !readBit()) { + leadingZeros++; + } + boolean hitLimit = byteOffset == byteLimit; + byteOffset = initialByteOffset; + bitOffset = initialBitOffset; + return !hitLimit && canReadBits(leadingZeros * 2 + 1); + } + + /** + * Reads an unsigned Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readUnsignedExpGolombCodedInt() { + return readExpGolombCodeNum(); + } + + /** + * Reads an signed Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readSignedExpGolombCodedInt() { + int codeNum = readExpGolombCodeNum(); + return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); + } + + private int readExpGolombCodeNum() { + int leadingZeros = 0; + while (!readBit()) { + leadingZeros++; + } + return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); + } + + private boolean shouldSkipByte(int offset) { + return 2 <= offset && offset < byteLimit && data[offset] == (byte) 0x03 + && data[offset - 2] == (byte) 0x00 && data[offset - 1] == (byte) 0x00; + } + + private void assertValidOffset() { + // It is fine for position to be at the end of the array, but no further. + Assertions.checkState(byteOffset >= 0 + && (bitOffset >= 0 && bitOffset < 8) + && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); + } + +}