Add support for H.263 and MPEG-4 Part 2 in TS
The new reader is named H263Reader as it handles H.263 streams, but MPEG-4 Part 2 streams are also intended to be handled. The reader's output format MIME type is video/mp4v as the H.263 streams can be decoded by decoders supporting this MIME type. The implementation is based on the framework implementation for extracting MPEG-4 video in MPEG-TS (https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/mpeg2ts/ESQueue.cpp;l=1825;drc=86e363c1fac27302ca4ae33e73296f7797672995) and is similar to the existing H262Reader. Issue: #1603 Issue: #5107 PiperOrigin-RevId: 320565337
This commit is contained in:
parent
a8f1cdcfd7
commit
1e2ed51f25
@ -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.
|
||||
|
@ -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),
|
||||
|
@ -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<Format, Long> result = parseCsdBuffer(csdBuffer, formatId);
|
||||
Pair<Format, Long> 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<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) {
|
||||
private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
|
||||
byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
|
||||
|
||||
int firstByte = csdData[4] & 0xFF;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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(
|
||||
|
BIN
testdata/src/test/assets/ts/sample_h263.ts
vendored
Normal file
BIN
testdata/src/test/assets/ts/sample_h263.ts
vendored
Normal file
Binary file not shown.
121
testdata/src/test/assets/ts/sample_h263.ts.0.dump
vendored
Normal file
121
testdata/src/test/assets/ts/sample_h263.ts.0.dump
vendored
Normal file
@ -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
|
97
testdata/src/test/assets/ts/sample_h263.ts.1.dump
vendored
Normal file
97
testdata/src/test/assets/ts/sample_h263.ts.1.dump
vendored
Normal file
@ -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
|
57
testdata/src/test/assets/ts/sample_h263.ts.2.dump
vendored
Normal file
57
testdata/src/test/assets/ts/sample_h263.ts.2.dump
vendored
Normal file
@ -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
|
25
testdata/src/test/assets/ts/sample_h263.ts.3.dump
vendored
Normal file
25
testdata/src/test/assets/ts/sample_h263.ts.3.dump
vendored
Normal file
@ -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
|
118
testdata/src/test/assets/ts/sample_h263.ts.unknown_length.dump
vendored
Normal file
118
testdata/src/test/assets/ts/sample_h263.ts.unknown_length.dump
vendored
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user