From 3208e2067335c7de94eed2d69ca2da841c62bb05 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 1 Apr 2016 07:33:59 -0700 Subject: [PATCH] Add ParsableNalUnitBitArray which unescapes while reading. This replaces calls to unescape except for SEI unescaping. Use the new ParsableNalUnitBitArray for reading the slice header in HLS access unit detection and slice_type reading. Unescape the SPS before parsing in FLV and MP4. Before this change it was parsed in its original (escaped) form. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=118777869 --- .../exoplayer/util/NalUnitUtilTest.java | 21 ++ .../util/ParsableNalUnitBitArrayTest.java | 114 +++++++++ .../extractor/flv/VideoTagPayloadReader.java | 15 +- .../exoplayer/extractor/mp4/AtomParsers.java | 8 +- .../exoplayer/extractor/ts/H264Reader.java | 79 +++--- .../exoplayer/extractor/ts/H265Reader.java | 11 +- .../android/exoplayer/util/NalUnitUtil.java | 20 +- .../exoplayer/util/ParsableBitArray.java | 46 ---- .../util/ParsableNalUnitBitArray.java | 226 ++++++++++++++++++ 9 files changed, 424 insertions(+), 116 deletions(-) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/util/ParsableNalUnitBitArrayTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer/util/ParsableNalUnitBitArray.java 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))); + } + +}