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 1125b44f3f..c9dcb1fa7d 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 @@ -77,7 +77,7 @@ import java.util.List; private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer sei; - private boolean writingSample; + private boolean foundFirstSample; private long totalBytesWritten; // Per sample state that gets reset at the start of each sample. @@ -111,7 +111,7 @@ import java.util.List; if (ifrParserBuffer != null) { ifrParserBuffer.reset(); } - writingSample = false; + foundFirstSample = false; totalBytesWritten = 0; } @@ -146,7 +146,7 @@ import java.util.List; isKeyframe = true; break; case NAL_UNIT_TYPE_AUD: - if (writingSample) { + if (foundFirstSample) { if (ifrParserBuffer != null && ifrParserBuffer.isCompleted()) { int sliceType = ifrParserBuffer.getSliceType(); isKeyframe |= (sliceType == FRAME_TYPE_I || sliceType == FRAME_TYPE_ALL_I); @@ -158,9 +158,8 @@ import java.util.List; int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0; int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit; output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null); - writingSample = false; } - writingSample = true; + foundFirstSample = true; samplePosition = totalBytesWritten - bytesWrittenPastNalUnit; sampleTimeUs = pesTimeUs; isKeyframe = false; @@ -215,6 +214,7 @@ import java.util.List; if (sei.endNalUnit(discardPadding)) { int unescapedLength = unescapeStream(sei.nalData, sei.nalLength); seiWrapper.reset(sei.nalData, unescapedLength); + seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. seiReader.consume(seiWrapper, pesTimeUs, true); } } @@ -385,7 +385,7 @@ import java.util.List; return unescapedLength; } - private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + 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; 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 new file mode 100644 index 0000000000..9728c59e1f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java @@ -0,0 +1,555 @@ +/* + * 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.extractor.ts; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +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 android.util.Log; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Parses a continuous H.265 byte stream and extracts individual frames. + */ +/* package */ class H265Reader extends ElementaryStreamReader { + + private static final String TAG = "H265Reader"; + + // nal_unit_type values from H.265/HEVC (2014) Table 7-1. + private static final int BLA_W_LP = 16; + private static final int BLA_W_RADL = 17; + private static final int BLA_N_LP = 18; + private static final int IDR_W_RADL = 19; + private static final int IDR_N_LP = 20; + private static final int CRA_NUT = 21; + + private static final int VPS_NUT = 32; + private static final int SPS_NUT = 33; + private static final int PPS_NUT = 34; + + private static final int PREFIX_SEI_NUT = 39; + private static final int SUFFIX_SEI_NUT = 40; + + // TODO: Deduplicate with H264Reader. + private static final int EXTENDED_SAR = 0xFF; + private static final float[] ASPECT_RATIO_IDC_VALUES = new float[] { + 1f /* Unspecified. Assume square */, + 1f, + 12f / 11f, + 10f / 11f, + 16f / 11f, + 40f / 33f, + 24f / 11f, + 20f / 11f, + 32f / 11f, + 80f / 33f, + 18f / 11f, + 15f / 11f, + 64f / 33f, + 160f / 99f, + 4f / 3f, + 3f / 2f, + 2f + }; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // State that should be reset on seek. + private final SeiReader seiReader; + private final boolean[] prefixFlags; + private final NalUnitTargetBuffer vps; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer prefixSei; + private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? + private boolean foundFirstSample; + private long totalBytesWritten; + + // Per sample state that gets reset at the start of each sample. + private boolean isKeyframe; + private long samplePosition; + private long sampleTimeUs; + + // Scratch variables to avoid allocations. + private final ParsableByteArray seiWrapper; + private int[] scratchEscapePositions; + + public H265Reader(TrackOutput output, SeiReader seiReader) { + super(output); + this.seiReader = seiReader; + prefixFlags = new boolean[3]; + vps = new NalUnitTargetBuffer(VPS_NUT, 128); + sps = new NalUnitTargetBuffer(SPS_NUT, 128); + pps = new NalUnitTargetBuffer(PPS_NUT, 128); + prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128); + suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128); + seiWrapper = new ParsableByteArray(); + scratchEscapePositions = new int[10]; + } + + @Override + public void seek() { + seiReader.seek(); + NalUnitUtil.clearPrefixFlags(prefixFlags); + vps.reset(); + sps.reset(); + pps.reset(); + prefixSei.reset(); + suffixSei.reset(); + foundFirstSample = false; + totalBytesWritten = 0; + } + + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + while (data.bytesLeft() > 0) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (offset < limit) { + int nextNalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + if (nextNalUnitOffset < limit) { + // We've seen the start of a NAL unit. + + // This is the length to the start of the unit. It may be negative if the NAL unit + // actually started in previously consumed data. + int lengthToNalUnit = nextNalUnitOffset - offset; + if (lengthToNalUnit > 0) { + feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset); + } + + int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nextNalUnitOffset); + int bytesWrittenPastNalUnit = limit - nextNalUnitOffset; + isKeyframe |= isRandomAccessPoint(nalUnitType); + + // Output sample data for VCL NAL units. + if (isInVcl(nalUnitType)) { + if (foundFirstSample) { + if (isKeyframe && !hasOutputFormat && vps.isCompleted() && sps.isCompleted() + && pps.isCompleted()) { + parseMediaFormat(vps, sps, pps); + } + int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0; + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null); + } + foundFirstSample = true; + samplePosition = totalBytesWritten - bytesWrittenPastNalUnit; + sampleTimeUs = pesTimeUs; + isKeyframe = false; + } + + // If the length to the start of the unit is negative then we wrote too many bytes to the + // NAL buffers. Discard the excess bytes when notifying that the unit has ended. + feedNalUnitTargetEnd(pesTimeUs, lengthToNalUnit < 0 ? -lengthToNalUnit : 0); + // Notify the start of the next NAL unit. + feedNalUnitTargetBuffersStart(nalUnitType); + // Continue scanning the data. + offset = nextNalUnitOffset + 3; + } else { + feedNalUnitTargetBuffersData(dataArray, offset, limit); + offset = limit; + } + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + private void feedNalUnitTargetBuffersStart(int nalUnitType) { + if (!hasOutputFormat) { + vps.startNalUnit(nalUnitType); + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + prefixSei.startNalUnit(nalUnitType); + suffixSei.startNalUnit(nalUnitType); + } + + private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { + if (!hasOutputFormat) { + vps.appendToNalUnit(dataArray, offset, limit); + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + prefixSei.appendToNalUnit(dataArray, offset, limit); + suffixSei.appendToNalUnit(dataArray, offset, limit); + } + + private void feedNalUnitTargetEnd(long pesTimeUs, int discardPadding) { + vps.endNalUnit(discardPadding); + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (prefixSei.endNalUnit(discardPadding)) { + int unescapedLength = unescapeStream(prefixSei.nalData, prefixSei.nalLength); + seiWrapper.reset(prefixSei.nalData, unescapedLength); + + // Skip the NAL prefix and type. + seiWrapper.skipBytes(5); + seiReader.consume(seiWrapper, pesTimeUs, true); + } + if (suffixSei.endNalUnit(discardPadding)) { + int unescapedLength = unescapeStream(suffixSei.nalData, suffixSei.nalLength); + seiWrapper.reset(suffixSei.nalData, unescapedLength); + + // Skip the NAL prefix and type. + seiWrapper.skipBytes(5); + seiReader.consume(seiWrapper, pesTimeUs, true); + } + } + + private void parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, + NalUnitTargetBuffer pps) { + // Build codec-specific data. + byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; + System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); + 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. + 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); + bitArray.skipBits(1); // sps_temporal_id_nesting_flag + + // profile_tier_level(1, sps_max_sub_layers_minus1) + bitArray.skipBits(88); // if (profilePresentFlag) {...} + bitArray.skipBits(8); // general_level_idc + int toSkip = 0; + for (int i = 0; i < maxSubLayersMinus1; i++) { + if (bitArray.readBits(1) == 1) { // sub_layer_profile_present_flag[i] + toSkip += 89; + } + if (bitArray.readBits(1) == 1) { // sub_layer_level_present_flag[i] + toSkip += 8; + } + } + bitArray.skipBits(toSkip); + if (maxSubLayersMinus1 > 0) { + bitArray.skipBits(2 * (8 - maxSubLayersMinus1)); + } + + bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id + int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + bitArray.skipBits(1); // separate_colour_plane_flag + } + int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); + int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); + if (bitArray.readBit()) { // conformance_window_flag + int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); + // H.265/HEVC (2014) Table 6-1 + int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1; + int subHeightC = chromaFormatIdc == 1 ? 2 : 1; + picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset); + picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset); + } + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt(); + // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...) + for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i] + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i] + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i] + } + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size + bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter + bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra + // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}} + if (bitArray.readBit() && bitArray.readBit()) { + skipScalingList(bitArray); + } + bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1) + if (bitArray.readBit()) { // pcm_enabled_flag + // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4) + bitArray.skipBits(4); + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size + bitArray.skipBits(1); // pcm_loop_filter_disabled_flag + } + // Skips all short term reference picture sets. + skipShortTermRefPicSets(bitArray); + if (bitArray.readBit()) { // long_term_ref_pics_present_flag + // num_long_term_ref_pics_sps + for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) { + int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4; + // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i] + bitArray.skipBits(ltRefPicPocLsbSpsLength + 1); + } + } + bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag + float pixelWidthHeightRatio = 1; + if (bitArray.readBit()) { // vui_parameters_present_flag + if (bitArray.readBit()) { // aspect_ratio_info_present_flag + int aspectRatioIdc = bitArray.readBits(8); + if (aspectRatioIdc == EXTENDED_SAR) { + int sarWidth = bitArray.readBits(16); + int sarHeight = bitArray.readBits(16); + if (sarWidth != 0 && sarHeight != 0) { + pixelWidthHeightRatio = (float) sarWidth / sarHeight; + } + } else if (aspectRatioIdc < ASPECT_RATIO_IDC_VALUES.length) { + pixelWidthHeightRatio = ASPECT_RATIO_IDC_VALUES[aspectRatioIdc]; + } else { + Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc); + } + } + } + + output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H265, MediaFormat.NO_VALUE, + C.UNKNOWN_TIME_US, picWidthInLumaSamples, picHeightInLumaSamples, pixelWidthHeightRatio, + Collections.singletonList(csd))); + hasOutputFormat = true; + } + + /** Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4. */ + private void skipScalingList(ParsableBitArray 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] + // scaling_list_pred_matrix_id_delta[sizeId][matrixId] + bitArray.readUnsignedExpGolombCodedInt(); + } else { + int coefNum = Math.min(64, 1 << (4 + sizeId << 1)); + if (sizeId > 1) { + // scaling_list_dc_coef_minus8[sizeId − 2][matrixId] + bitArray.readSignedExpGolombCodedInt(); + } + for (int i = 0; i < coefNum; i++) { + bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef + } + } + } + } + } + + // 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 + || nalUnitType == IDR_W_RADL || nalUnitType == IDR_N_LP || nalUnitType == CRA_NUT; + } + + /** Returns whether the NAL unit is in the video coding layer. */ + private static boolean isInVcl(int nalUnitType) { + return nalUnitType <= VPS_NUT; + } + + /** + * 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) { + int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt(); + boolean interRefPicSetPredictionFlag = false; + int numNegativePics = 0; + int numPositivePics = 0; + // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous + // one, so we just keep track of that rather than storing the whole array. + // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS. + int previousNumDeltaPocs = 0; + for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) { + if (stRpsIdx != 0) { + interRefPicSetPredictionFlag = bitArray.readBit(); + } + if (interRefPicSetPredictionFlag) { + bitArray.skipBits(1); // delta_rps_sign + bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1 + for (int j = 0; j <= previousNumDeltaPocs; j++) { + if (bitArray.readBit()) { // used_by_curr_pic_flag[j] + bitArray.skipBits(1); // use_delta_flag[j] + } + } + } else { + numNegativePics = bitArray.readUnsignedExpGolombCodedInt(); + numPositivePics = bitArray.readUnsignedExpGolombCodedInt(); + previousNumDeltaPocs = numNegativePics + numPositivePics; + for (int i = 0; i < numNegativePics; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i] + bitArray.skipBits(1); // used_by_curr_pic_s0_flag[i] + } + for (int i = 0; i < numPositivePics; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i] + bitArray.skipBits(1); // used_by_curr_pic_s1_flag[i] + } + } + } + } + + // TODO: Deduplicate with H264Reader.NalUnitTargetBuffer. + /** + * A buffer that fills itself with data corresponding to a specific NAL unit, as it is + * encountered in the stream. + */ + private static final class NalUnitTargetBuffer { + + private final int targetType; + + private boolean isFilling; + private boolean isCompleted; + + public byte[] nalData; + public int nalLength; + + public NalUnitTargetBuffer(int targetType, int initialCapacity) { + this.targetType = targetType; + nalData = new byte[5 + initialCapacity]; + nalData[2] = 1; + } + + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + isCompleted = false; + } + + /** + * True if the buffer currently holds a complete NAL unit of the target type. + */ + public boolean isCompleted() { + return isCompleted; + } + + /** + * Invoked to indicate that a NAL unit has started. + * + * @param type The type of the NAL unit. + */ + public void startNalUnit(int type) { + Assertions.checkState(!isFilling); + isFilling = type == targetType; + if (isFilling) { + nalLength = 3; + isCompleted = false; + } + } + + /** + * Invoked to pass stream data. The data passed should not include 4 byte NAL unit prefixes. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (nalData.length < nalLength + readLength) { + nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2); + } + System.arraycopy(data, offset, nalData, nalLength, readLength); + nalLength += readLength; + } + + /** + * Invoked to indicate that a NAL unit has ended. + * + * @param discardPadding The number of excess bytes that were passed to + * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded. + * @return True if the ended NAL unit is of the target type. False otherwise. + */ + public boolean endNalUnit(int discardPadding) { + if (!isFilling) { + return false; + } + nalLength -= discardPadding; + isFilling = false; + isCompleted = true; + return true; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java index 1bd169d544..0283ceea92 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java @@ -41,9 +41,6 @@ import com.google.android.exoplayer.util.ParsableByteArray; @Override public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) { - // Skip the NAL prefix and type. - seiBuffer.skipBytes(4); - int b; while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { // Parse payload type. diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index a17dce7836..5a61852340 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -46,6 +46,7 @@ public final class TsExtractor implements Extractor, SeekMap { private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81; private static final int TS_STREAM_TYPE_ATSC_E_AC3 = 0x87; private static final int TS_STREAM_TYPE_H264 = 0x1B; + private static final int TS_STREAM_TYPE_H265 = 0x24; private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 @@ -361,9 +362,12 @@ public final class TsExtractor implements Extractor, SeekMap { pesPayloadReader = new Ac3Reader(output.track(streamType)); break; case TS_STREAM_TYPE_H264: - SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608)); - pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader, - idrKeyframesOnly); + pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), + new SeiReader(output.track(TS_STREAM_TYPE_EIA608)), idrKeyframesOnly); + break; + case TS_STREAM_TYPE_H265: + pesPayloadReader = new H265Reader(output.track(TS_STREAM_TYPE_H265), + new SeiReader(output.track(TS_STREAM_TYPE_EIA608))); break; case TS_STREAM_TYPE_ID3: pesPayloadReader = id3Reader; 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 be4ecc476f..0e390e4a94 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 @@ -64,6 +64,18 @@ public final class NalUnitUtil { return data[offset + 3] & 0x1F; } + /** + * Gets the type of the H.265 NAL unit in {@code data} that starts at {@code offset}. + * + * @param data The data to search. + * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and + * {@code data.length - 3} (exclusive). + * @return The type of the unit. + */ + public static int getH265NalUnitType(byte[] data, int offset) { + return (data[offset + 3] & 0x7E) >> 1; + } + /** * Finds the first NAL unit in {@code data}. *