diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c05b55dc6b..c975d64f89 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -212,6 +212,9 @@ headers mime type in `DefaultExtractorsFactory`. * Add support for partially fragmented MP4s ([#7308](https://github.com/google/ExoPlayer/issues/7308)). + * Add support for MPEG-4 Part 2 and H.263 in MPEG-TS + ([#1603](https://github.com/google/ExoPlayer/issues/1603), + [#5107](https://github.com/google/ExoPlayer/issues/5107)). * Testing * Add `TestExoPlayer`, a utility class with APIs to create `SimpleExoPlayer` instances with fake components for testing. diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index c48c790fbf..514151c2f4 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -165,6 +165,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader(buildUserDataReader(esInfo))); + case TsExtractor.TS_STREAM_TYPE_H263: + return new PesReader(new H263Reader(buildUserDataReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(new H264Reader(buildSeiReader(esInfo), diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 012de81297..a7cbd9c5d6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -22,7 +25,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -118,7 +120,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { - Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. + checkStateNotNull(output); // Asserts that createTracks has been called. int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -156,7 +158,7 @@ public final class H262Reader implements ElementaryStreamReader { int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer, formatId); + Pair result = parseCsdBuffer(csdBuffer, checkNotNull(formatId)); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -215,11 +217,11 @@ public final class H262Reader implements ElementaryStreamReader { * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. - * @param formatId The id for the generated format. May be null. + * @param formatId The id for the generated format. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if * the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java new file mode 100644 index 0000000000..bbe2f91d6f --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java @@ -0,0 +1,477 @@ +/* + * Copyright 2020 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.exoplayer2.extractor.ts; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.NalUnitUtil; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an ISO/IEC 14496-2 (MPEG-4 Part 2) or ITU-T Recommendation H.263 byte stream and extracts + * individual frames. + */ +public final class H263Reader implements ElementaryStreamReader { + + private static final String TAG = "H263Reader"; + + private static final int START_CODE_VALUE_VISUAL_OBJECT_SEQUENCE = 0xB0; + private static final int START_CODE_VALUE_USER_DATA = 0xB2; + private static final int START_CODE_VALUE_GROUP_OF_VOP = 0xB3; + private static final int START_CODE_VALUE_VISUAL_OBJECT = 0xB5; + private static final int START_CODE_VALUE_VOP = 0xB6; + private static final int START_CODE_VALUE_MAX_VIDEO_OBJECT = 0x1F; + private static final int START_CODE_VALUE_UNSET = -1; + + // See ISO 14496-2 (2001) table 6-12 for the mapping from aspect_ratio_info to pixel aspect ratio. + private static final float[] PIXEL_WIDTH_HEIGHT_RATIO_BY_ASPECT_RATIO_INFO = + new float[] {1f, 1f, 12 / 11f, 10 / 11f, 16 / 11f, 40 / 33f, 1f}; + private static final int VIDEO_OBJECT_LAYER_SHAPE_RECTANGULAR = 0; + + @Nullable private final UserDataReader userDataReader; + @Nullable private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + @Nullable private final NalUnitTargetBuffer userData; + private H263Reader.@MonotonicNonNull SampleReader sampleReader; + private long totalBytesWritten; + + // State initialized once when tracks are created. + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + + /** Creates a new reader. */ + public H263Reader() { + this(null); + } + + /* package */ H263Reader(@Nullable UserDataReader userDataReader) { + this.userDataReader = userDataReader; + prefixFlags = new boolean[4]; + csdBuffer = new CsdBuffer(128); + if (userDataReader != null) { + userData = new NalUnitTargetBuffer(START_CODE_VALUE_USER_DATA, 128); + userDataParsable = new ParsableByteArray(); + } else { + userData = null; + userDataParsable = null; + } + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + csdBuffer.reset(); + if (sampleReader != null) { + sampleReader.reset(); + } + if (userData != null) { + userData.reset(); + } + totalBytesWritten = 0; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + sampleReader = new SampleReader(output); + if (userDataReader != null) { + userDataReader.createTracks(extractorOutput, idGenerator); + } + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + // Assert that createTracks has been called. + checkStateNotNull(sampleReader); + checkStateNotNull(output); + 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()); + + while (true) { + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (startCodeOffset == limit) { + // We've scanned to the end of the data without finding another start code. + if (!hasOutputFormat) { + csdBuffer.onData(dataArray, offset, limit); + } + sampleReader.onData(dataArray, offset, limit); + if (userData != null) { + userData.appendToNalUnit(dataArray, offset, limit); + } + return; + } + + // We've found a start code with the following value. + int startCodeValue = data.data[startCodeOffset + 3] & 0xFF; + // This is the number of bytes from the current offset to the start of the next start + // code. It may be negative if the start code started in the previously consumed data. + int lengthToStartCode = startCodeOffset - offset; + + if (!hasOutputFormat) { + if (lengthToStartCode > 0) { + csdBuffer.onData(dataArray, offset, /* limit= */ startCodeOffset); + } + // This is the number of bytes belonging to the next start code that have already been + // passed to csdBuffer. + int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; + if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { + // The csd data is complete, so we can decode and output the media format. + output.format( + parseCsdBuffer(csdBuffer, csdBuffer.volStartPosition, checkNotNull(formatId))); + hasOutputFormat = true; + } + } + + sampleReader.onData(dataArray, offset, /* limit= */ startCodeOffset); + + if (userData != null) { + int bytesAlreadyPassed = 0; + if (lengthToStartCode > 0) { + userData.appendToNalUnit(dataArray, offset, /* limit= */ startCodeOffset); + } else { + bytesAlreadyPassed = -lengthToStartCode; + } + + if (userData.endNalUnit(bytesAlreadyPassed)) { + int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); + castNonNull(userDataParsable).reset(userData.nalData, unescapedLength); + castNonNull(userDataReader).consume(pesTimeUs, userDataParsable); + } + + if (startCodeValue == START_CODE_VALUE_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { + userData.startNalUnit(startCodeValue); + } + } + + int bytesWrittenPastPosition = limit - startCodeOffset; + long absolutePosition = totalBytesWritten - bytesWrittenPastPosition; + sampleReader.onDataEnd(absolutePosition, bytesWrittenPastPosition, hasOutputFormat); + // Indicate the start of the next chunk. + sampleReader.onStartCode(startCodeValue, pesTimeUs); + // Continue scanning the data. + offset = startCodeOffset + 3; + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses a codec-specific data buffer, returning the {@link Format} of the media. + * + * @param csdBuffer The buffer to parse. + * @param volStartPosition The byte offset of the start of the video object layer in the buffer. + * @param formatId The ID for the generated format. + * @return The {@link Format} of the media represented in the buffer. + */ + private static Format parseCsdBuffer(CsdBuffer csdBuffer, int volStartPosition, String formatId) { + byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); + ParsableBitArray buffer = new ParsableBitArray(csdData); + buffer.skipBytes(volStartPosition); + + // Parse the video object layer defined in ISO 14496-2 (2001) subsection 6.2.3. + buffer.skipBytes(4); // video_object_layer_start_code + buffer.skipBit(); // random_accessible_vol + buffer.skipBits(8); // video_object_type_indication + if (buffer.readBit()) { // is_object_layer_identifier + buffer.skipBits(4); // video_object_layer_verid + buffer.skipBits(3); // video_object_layer_priority + } + float pixelWidthHeightRatio; + int aspectRatioInfo = buffer.readBits(4); + if (aspectRatioInfo == 0x0F) { // extended_PAR + int parWidth = buffer.readBits(8); + int parHeight = buffer.readBits(8); + if (parHeight == 0) { + Log.w(TAG, "Invalid aspect ratio"); + pixelWidthHeightRatio = 1f; + } else { + pixelWidthHeightRatio = (float) parWidth / parHeight; + } + } else if (aspectRatioInfo < PIXEL_WIDTH_HEIGHT_RATIO_BY_ASPECT_RATIO_INFO.length) { + pixelWidthHeightRatio = PIXEL_WIDTH_HEIGHT_RATIO_BY_ASPECT_RATIO_INFO[aspectRatioInfo]; + } else { + Log.w(TAG, "Invalid aspect ratio"); + pixelWidthHeightRatio = 1f; + } + if (buffer.readBit()) { // vol_control_parameters + buffer.skipBits(2); // chroma_format + buffer.skipBits(1); // low_delay + if (buffer.readBit()) { // vbv_parameters + buffer.skipBits(15); // first_half_bit_rate + buffer.skipBit(); // marker_bit + buffer.skipBits(15); // latter_half_bit_rate + buffer.skipBit(); // marker_bit + buffer.skipBits(15); // first_half_vbv_buffer_size + buffer.skipBit(); // marker_bit + buffer.skipBits(3); // latter_half_vbv_buffer_size + buffer.skipBits(11); // first_half_vbv_occupancy + buffer.skipBit(); // marker_bit + buffer.skipBits(15); // latter_half_vbv_occupancy + buffer.skipBit(); // marker_bit + } + } + int videoObjectLayerShape = buffer.readBits(2); + if (videoObjectLayerShape != VIDEO_OBJECT_LAYER_SHAPE_RECTANGULAR) { + Log.w(TAG, "Unhandled video object layer shape"); + } + buffer.skipBit(); // marker_bit + int vopTimeIncrementResolution = buffer.readBits(16); + buffer.skipBit(); // marker_bit + if (buffer.readBit()) { // fixed_vop_rate + if (vopTimeIncrementResolution == 0) { + Log.w(TAG, "Invalid vop_increment_time_resolution"); + } else { + vopTimeIncrementResolution--; + int numBits = 0; + while (vopTimeIncrementResolution > 0) { + ++numBits; + vopTimeIncrementResolution >>= 1; + } + buffer.skipBits(numBits); // fixed_vop_time_increment + } + } + buffer.skipBit(); // marker_bit + int videoObjectLayerWidth = buffer.readBits(13); + buffer.skipBit(); // marker_bit + int videoObjectLayerHeight = buffer.readBits(13); + buffer.skipBit(); // marker_bit + buffer.skipBit(); // interlaced + return new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.VIDEO_MP4V) + .setWidth(videoObjectLayerWidth) + .setHeight(videoObjectLayerHeight) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setInitializationData(Collections.singletonList(csdData)) + .build(); + } + + private static final class CsdBuffer { + + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START, + STATE_EXPECT_VISUAL_OBJECT_START, + STATE_EXPECT_VIDEO_OBJECT_START, + STATE_EXPECT_VIDEO_OBJECT_LAYER_START, + STATE_WAIT_FOR_VOP_START + }) + private @interface State {} + + private static final int STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START = 0; + private static final int STATE_EXPECT_VISUAL_OBJECT_START = 1; + private static final int STATE_EXPECT_VIDEO_OBJECT_START = 2; + private static final int STATE_EXPECT_VIDEO_OBJECT_LAYER_START = 3; + private static final int STATE_WAIT_FOR_VOP_START = 4; + + private boolean isFilling; + @State private int state; + + public int length; + public int volStartPosition; + public byte[] data; + + public CsdBuffer(int initialCapacity) { + data = new byte[initialCapacity]; + } + + public void reset() { + isFilling = false; + length = 0; + state = STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START; + } + + /** + * Called when a start code is encountered in the stream. + * + * @param startCodeValue The start code value. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. + * @return Whether the csd data is now complete. If true is returned, neither this method nor + * {@link #onData(byte[], int, int)} should be called again without an interleaving call to + * {@link #reset()}. + */ + public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { + switch (state) { + case STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START: + if (startCodeValue == START_CODE_VALUE_VISUAL_OBJECT_SEQUENCE) { + state = STATE_EXPECT_VISUAL_OBJECT_START; + isFilling = true; + } + break; + case STATE_EXPECT_VISUAL_OBJECT_START: + if (startCodeValue != START_CODE_VALUE_VISUAL_OBJECT) { + Log.w(TAG, "Unexpected start code value"); + reset(); + } else { + state = STATE_EXPECT_VIDEO_OBJECT_START; + } + break; + case STATE_EXPECT_VIDEO_OBJECT_START: + if (startCodeValue > START_CODE_VALUE_MAX_VIDEO_OBJECT) { + Log.w(TAG, "Unexpected start code value"); + reset(); + } else { + state = STATE_EXPECT_VIDEO_OBJECT_LAYER_START; + } + break; + case STATE_EXPECT_VIDEO_OBJECT_LAYER_START: + if ((startCodeValue & 0xF0) != 0x20) { + Log.w(TAG, "Unexpected start code value"); + reset(); + } else { + volStartPosition = length; + state = STATE_WAIT_FOR_VOP_START; + } + break; + case STATE_WAIT_FOR_VOP_START: + if (startCodeValue == START_CODE_VALUE_GROUP_OF_VOP + || startCodeValue == START_CODE_VALUE_VISUAL_OBJECT) { + length -= bytesAlreadyPassed; + isFilling = false; + return true; + } + break; + default: + throw new IllegalStateException(); + } + onData(START_CODE, /* offset= */ 0, /* limit= */ START_CODE.length); + return false; + } + + public void onData(byte[] newData, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (data.length < length + readLength) { + data = Arrays.copyOf(data, (length + readLength) * 2); + } + System.arraycopy(newData, offset, data, length, readLength); + length += readLength; + } + } + + private static final class SampleReader { + + /** Byte offset of vop_coding_type after the start code value. */ + private static final int OFFSET_VOP_CODING_TYPE = 1; + /** Value of vop_coding_type for intra video object planes. */ + private static final int VOP_CODING_TYPE_INTRA = 0; + + private final TrackOutput output; + + private boolean readingSample; + private boolean lookingForVopCodingType; + private boolean sampleIsKeyframe; + private int startCodeValue; + private int vopBytesRead; + private long samplePosition; + private long sampleTimeUs; + + public SampleReader(TrackOutput output) { + this.output = output; + } + + public void reset() { + readingSample = false; + lookingForVopCodingType = false; + sampleIsKeyframe = false; + startCodeValue = START_CODE_VALUE_UNSET; + } + + public void onStartCode(int startCodeValue, long pesTimeUs) { + this.startCodeValue = startCodeValue; + sampleIsKeyframe = false; + readingSample = + startCodeValue == START_CODE_VALUE_VOP || startCodeValue == START_CODE_VALUE_GROUP_OF_VOP; + lookingForVopCodingType = startCodeValue == START_CODE_VALUE_VOP; + vopBytesRead = 0; + sampleTimeUs = pesTimeUs; + } + + public void onData(byte[] data, int offset, int limit) { + if (lookingForVopCodingType) { + int headerOffset = offset + OFFSET_VOP_CODING_TYPE - vopBytesRead; + if (headerOffset < limit) { + sampleIsKeyframe = ((data[headerOffset] & 0xC0) >> 6) == VOP_CODING_TYPE_INTRA; + lookingForVopCodingType = false; + } else { + vopBytesRead += limit - offset; + } + } + } + + public void onDataEnd(long position, int bytesWrittenPastPosition, boolean hasOutputFormat) { + if (startCodeValue == START_CODE_VALUE_VOP && hasOutputFormat && readingSample) { + int size = (int) (position - samplePosition); + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + output.sampleMetadata( + sampleTimeUs, flags, size, bytesWrittenPastPosition, /* encryptionData= */ null); + } + // Start a new sample, unless this is a 'group of video object plane' in which case we + // include the data at the start of a 'video object plane' coming next. + if (startCodeValue != START_CODE_VALUE_GROUP_OF_VOP) { + samplePosition = position; + } + } + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 7b470ad570..9734cab1ca 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -90,6 +90,7 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_E_AC3 = 0x87; public static final int TS_STREAM_TYPE_AC4 = 0xAC; // DVB/ATSC AC-4 Descriptor public static final int TS_STREAM_TYPE_H262 = 0x02; + public static final int TS_STREAM_TYPE_H263 = 0x10; // MPEG-4 Part 2 and H.263 public static final int TS_STREAM_TYPE_H264 = 0x1B; public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 5ef5fc9048..4b198f23ed 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -60,6 +60,11 @@ public final class TsExtractorTest { TsExtractor::new, "ts/sample_h262_mpeg_audio.ts", simulationConfig); } + @Test + public void sampleWithH263() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h263.ts", simulationConfig); + } + @Test public void sampleWithH264AndMpegAudio() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/testdata/src/test/assets/ts/sample_h263.ts b/testdata/src/test/assets/ts/sample_h263.ts new file mode 100644 index 0000000000..83eecc11e9 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_h263.ts differ diff --git a/testdata/src/test/assets/ts/sample_h263.ts.0.dump b/testdata/src/test/assets/ts/sample_h263.ts.0.dump new file mode 100644 index 0000000000..b1e959c84e --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h263.ts.0.dump @@ -0,0 +1,121 @@ +seekMap: + isSeekable = true + duration = 960000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(480000) = [[timeUs=480000, position=21958]] + getPosition(960000) = [[timeUs=960000, position=44104]] +numberOfTracks = 2 +track 256: + total output bytes = 39002 + sample count = 24 + format 0: + id = 1/256 + sampleMimeType = video/mp4v-es + width = 640 + height = 360 + initializationData: + data = length 47, hash 7696BF67 + sample 0: + time = 0 + flags = 1 + data = length 8408, hash 718A7985 + sample 1: + time = 40000 + flags = 0 + data = length 2018, hash 7BC8193F + sample 2: + time = 80000 + flags = 0 + data = length 480, hash C244FFAF + sample 3: + time = 120000 + flags = 0 + data = length 256, hash 56D68D82 + sample 4: + time = 160000 + flags = 0 + data = length 222, hash FADF6CA9 + sample 5: + time = 200000 + flags = 0 + data = length 217, hash 161BB856 + sample 6: + time = 240000 + flags = 0 + data = length 212, hash 835B0727 + sample 7: + time = 280000 + flags = 0 + data = length 212, hash E9AF0AB7 + sample 8: + time = 320000 + flags = 0 + data = length 212, hash E9517D06 + sample 9: + time = 360000 + flags = 0 + data = length 212, hash 4FA58096 + sample 10: + time = 400000 + flags = 0 + data = length 212, hash 4F47F2E5 + sample 11: + time = 440000 + flags = 0 + data = length 212, hash B59BF675 + sample 12: + time = 480000 + flags = 1 + data = length 11769, hash 3ED9DF06 + sample 13: + time = 520000 + flags = 0 + data = length 230, hash 2AF3505D + sample 14: + time = 560000 + flags = 0 + data = length 222, hash F4E7436D + sample 15: + time = 600000 + flags = 0 + data = length 222, hash F0F812FD + sample 16: + time = 640000 + flags = 0 + data = length 222, hash 18472E8C + sample 17: + time = 680000 + flags = 0 + data = length 222, hash 1457FE1C + sample 18: + time = 720000 + flags = 0 + data = length 222, hash 3BA719AB + sample 19: + time = 760000 + flags = 0 + data = length 222, hash 37B7E93B + sample 20: + time = 800000 + flags = 0 + data = length 222, hash 5F0704CA + sample 21: + time = 840000 + flags = 0 + data = length 222, hash 5B17D45A + sample 22: + time = 880000 + flags = 0 + data = length 222, hash 8266EFE9 + sample 23: + time = 920000 + flags = 0 + data = length 222, hash 7E77BF79 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h263.ts.1.dump b/testdata/src/test/assets/ts/sample_h263.ts.1.dump new file mode 100644 index 0000000000..6b67aade74 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h263.ts.1.dump @@ -0,0 +1,97 @@ +seekMap: + isSeekable = true + duration = 960000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(480000) = [[timeUs=480000, position=21958]] + getPosition(960000) = [[timeUs=960000, position=44104]] +numberOfTracks = 2 +track 256: + total output bytes = 27354 + sample count = 18 + format 0: + id = 1/256 + sampleMimeType = video/mp4v-es + width = 640 + height = 360 + initializationData: + data = length 47, hash 7696BF67 + sample 0: + time = 320000 + flags = 0 + data = length 212, hash 835B0727 + sample 1: + time = 360000 + flags = 0 + data = length 212, hash E9AF0AB7 + sample 2: + time = 400000 + flags = 0 + data = length 212, hash E9517D06 + sample 3: + time = 440000 + flags = 0 + data = length 212, hash 4FA58096 + sample 4: + time = 480000 + flags = 0 + data = length 212, hash 4F47F2E5 + sample 5: + time = 520000 + flags = 0 + data = length 212, hash B59BF675 + sample 6: + time = 560000 + flags = 1 + data = length 11769, hash 3ED9DF06 + sample 7: + time = 600000 + flags = 0 + data = length 230, hash 2AF3505D + sample 8: + time = 640000 + flags = 0 + data = length 222, hash F4E7436D + sample 9: + time = 680000 + flags = 0 + data = length 222, hash F0F812FD + sample 10: + time = 720000 + flags = 0 + data = length 222, hash 18472E8C + sample 11: + time = 760000 + flags = 0 + data = length 222, hash 1457FE1C + sample 12: + time = 800000 + flags = 0 + data = length 222, hash 3BA719AB + sample 13: + time = 840000 + flags = 0 + data = length 222, hash 37B7E93B + sample 14: + time = 880000 + flags = 0 + data = length 222, hash 5F0704CA + sample 15: + time = 920000 + flags = 0 + data = length 222, hash 5B17D45A + sample 16: + time = 960000 + flags = 0 + data = length 222, hash 8266EFE9 + sample 17: + time = 1000000 + flags = 0 + data = length 222, hash 7E77BF79 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h263.ts.2.dump b/testdata/src/test/assets/ts/sample_h263.ts.2.dump new file mode 100644 index 0000000000..1bdb963353 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h263.ts.2.dump @@ -0,0 +1,57 @@ +seekMap: + isSeekable = true + duration = 960000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(480000) = [[timeUs=480000, position=21958]] + getPosition(960000) = [[timeUs=960000, position=44104]] +numberOfTracks = 2 +track 256: + total output bytes = 13592 + sample count = 8 + format 0: + id = 1/256 + sampleMimeType = video/mp4v-es + width = 640 + height = 360 + initializationData: + data = length 47, hash 7696BF67 + sample 0: + time = 640000 + flags = 0 + data = length 222, hash 18472E8C + sample 1: + time = 680000 + flags = 0 + data = length 222, hash 1457FE1C + sample 2: + time = 720000 + flags = 0 + data = length 222, hash 3BA719AB + sample 3: + time = 760000 + flags = 0 + data = length 222, hash 37B7E93B + sample 4: + time = 800000 + flags = 0 + data = length 222, hash 5F0704CA + sample 5: + time = 840000 + flags = 0 + data = length 222, hash 5B17D45A + sample 6: + time = 880000 + flags = 0 + data = length 222, hash 8266EFE9 + sample 7: + time = 920000 + flags = 0 + data = length 222, hash 7E77BF79 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h263.ts.3.dump b/testdata/src/test/assets/ts/sample_h263.ts.3.dump new file mode 100644 index 0000000000..3499ca254f --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h263.ts.3.dump @@ -0,0 +1,25 @@ +seekMap: + isSeekable = true + duration = 960000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(480000) = [[timeUs=480000, position=21958]] + getPosition(960000) = [[timeUs=960000, position=44104]] +numberOfTracks = 2 +track 256: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/256 + sampleMimeType = video/mp4v-es + width = 640 + height = 360 + initializationData: + data = length 47, hash 7696BF67 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h263.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_h263.ts.unknown_length.dump new file mode 100644 index 0000000000..d398c2e77b --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h263.ts.unknown_length.dump @@ -0,0 +1,118 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 256: + total output bytes = 39002 + sample count = 24 + format 0: + id = 1/256 + sampleMimeType = video/mp4v-es + width = 640 + height = 360 + initializationData: + data = length 47, hash 7696BF67 + sample 0: + time = 0 + flags = 1 + data = length 8408, hash 718A7985 + sample 1: + time = 40000 + flags = 0 + data = length 2018, hash 7BC8193F + sample 2: + time = 80000 + flags = 0 + data = length 480, hash C244FFAF + sample 3: + time = 120000 + flags = 0 + data = length 256, hash 56D68D82 + sample 4: + time = 160000 + flags = 0 + data = length 222, hash FADF6CA9 + sample 5: + time = 200000 + flags = 0 + data = length 217, hash 161BB856 + sample 6: + time = 240000 + flags = 0 + data = length 212, hash 835B0727 + sample 7: + time = 280000 + flags = 0 + data = length 212, hash E9AF0AB7 + sample 8: + time = 320000 + flags = 0 + data = length 212, hash E9517D06 + sample 9: + time = 360000 + flags = 0 + data = length 212, hash 4FA58096 + sample 10: + time = 400000 + flags = 0 + data = length 212, hash 4F47F2E5 + sample 11: + time = 440000 + flags = 0 + data = length 212, hash B59BF675 + sample 12: + time = 480000 + flags = 1 + data = length 11769, hash 3ED9DF06 + sample 13: + time = 520000 + flags = 0 + data = length 230, hash 2AF3505D + sample 14: + time = 560000 + flags = 0 + data = length 222, hash F4E7436D + sample 15: + time = 600000 + flags = 0 + data = length 222, hash F0F812FD + sample 16: + time = 640000 + flags = 0 + data = length 222, hash 18472E8C + sample 17: + time = 680000 + flags = 0 + data = length 222, hash 1457FE1C + sample 18: + time = 720000 + flags = 0 + data = length 222, hash 3BA719AB + sample 19: + time = 760000 + flags = 0 + data = length 222, hash 37B7E93B + sample 20: + time = 800000 + flags = 0 + data = length 222, hash 5F0704CA + sample 21: + time = 840000 + flags = 0 + data = length 222, hash 5B17D45A + sample 22: + time = 880000 + flags = 0 + data = length 222, hash 8266EFE9 + sample 23: + time = 920000 + flags = 0 + data = length 222, hash 7E77BF79 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true