Supports seeking for TS Streams.
This CL adds support for seeking witin TS streams by using binary search. For any seek timestamp, it tries to find the location in the stream where PCR timestamp is close to the target timestamp, and return this position as the seek position. Github: #966. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=207529906
This commit is contained in:
parent
7fcd6b6d65
commit
077f2c3036
@ -4,7 +4,10 @@
|
||||
|
||||
* Add `AudioListener` for listening to changes in audio configuration during
|
||||
playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).
|
||||
* MPEG-TS: Support CEA-608/708 in H262
|
||||
* MPEG-TS:
|
||||
* Support seeking for MPEG-TS Streams
|
||||
([#966](https://github.com/google/ExoPlayer/issues/966)).
|
||||
* Support CEA-608/708 in H262
|
||||
([#2565](https://github.com/google/ExoPlayer/issues/2565)).
|
||||
* MPEG-PS: Support reading duration and seeking for MPEG-PS Streams
|
||||
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
|
||||
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A seeker that supports seeking within TS stream using binary search.
|
||||
*
|
||||
* <p>This seeker uses the first and last PCR values within the stream, as well as the stream
|
||||
* duration to interpolate the PCR value of the seeking position. Then it performs binary search
|
||||
* within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the
|
||||
* target PCR.
|
||||
*/
|
||||
/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {
|
||||
|
||||
private static final long SEEK_TOLERANCE_US = 100_000;
|
||||
private static final int MINIMUM_SEARCH_RANGE_BYTES = TsExtractor.TS_PACKET_SIZE * 5;
|
||||
private static final int TIMESTAMP_SEARCH_PACKETS = 200;
|
||||
private static final int TIMESTAMP_SEARCH_BYTES =
|
||||
TsExtractor.TS_PACKET_SIZE * TIMESTAMP_SEARCH_PACKETS;
|
||||
|
||||
public TsBinarySearchSeeker(
|
||||
TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
|
||||
super(
|
||||
new DefaultSeekTimestampConverter(),
|
||||
new TsPcrSeeker(pcrPid, pcrTimestampAdjuster),
|
||||
streamDurationUs,
|
||||
/* floorTimePosition= */ 0,
|
||||
/* ceilingTimePosition= */ streamDurationUs + 1,
|
||||
/* floorBytePosition= */ 0,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
|
||||
MINIMUM_SEARCH_RANGE_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given
|
||||
* position in a TS stream.
|
||||
*
|
||||
* <p>Given a PCR timestamp, and a position within a TS stream, this seeker will try to read up to
|
||||
* {@link #TIMESTAMP_SEARCH_PACKETS} TS packets from that stream position, look for all packet
|
||||
* with PID equals to PCR_PID, and then compare the PCR timestamps (if available) of these packets
|
||||
* vs the target timestamp.
|
||||
*/
|
||||
private static final class TsPcrSeeker implements TimestampSeeker {
|
||||
|
||||
private final TimestampAdjuster pcrTimestampAdjuster;
|
||||
private final ParsableByteArray packetBuffer;
|
||||
private final int pcrPid;
|
||||
|
||||
public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {
|
||||
this.pcrPid = pcrPid;
|
||||
this.pcrTimestampAdjuster = pcrTimestampAdjuster;
|
||||
packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimestampSearchResult searchForTimestamp(
|
||||
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
|
||||
throws IOException, InterruptedException {
|
||||
long inputPosition = input.getPosition();
|
||||
int bytesToRead =
|
||||
(int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
|
||||
packetBuffer.reset(bytesToRead);
|
||||
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
|
||||
|
||||
return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
|
||||
}
|
||||
|
||||
private TimestampSearchResult searchForPcrValueInBuffer(
|
||||
ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) {
|
||||
int limit = packetBuffer.limit();
|
||||
|
||||
long startOfLastPacketPosition = C.POSITION_UNSET;
|
||||
long endOfLastPacketPosition = C.POSITION_UNSET;
|
||||
long lastPcrTimeUsInRange = C.TIME_UNSET;
|
||||
|
||||
while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) {
|
||||
int startOfPacket =
|
||||
TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit);
|
||||
int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE;
|
||||
if (endOfPacket > limit) {
|
||||
break;
|
||||
}
|
||||
long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid);
|
||||
if (pcrValue != C.TIME_UNSET) {
|
||||
long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue);
|
||||
if (pcrTimeUs > targetPcrTimeUs) {
|
||||
if (lastPcrTimeUsInRange == C.TIME_UNSET) {
|
||||
// First PCR timestamp is already over target.
|
||||
return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset);
|
||||
} else {
|
||||
// Last PCR timestamp < target timestamp < this timestamp.
|
||||
return TimestampSearchResult.targetFoundResult(
|
||||
bufferStartOffset + startOfLastPacketPosition);
|
||||
}
|
||||
} else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) {
|
||||
long startOfPacketInStream = bufferStartOffset + startOfPacket;
|
||||
return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
|
||||
}
|
||||
|
||||
lastPcrTimeUsInRange = pcrTimeUs;
|
||||
startOfLastPacketPosition = startOfPacket;
|
||||
}
|
||||
packetBuffer.setPosition(endOfPacket);
|
||||
endOfLastPacketPosition = endOfPacket;
|
||||
}
|
||||
|
||||
if (lastPcrTimeUsInRange != C.TIME_UNSET) {
|
||||
long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
|
||||
return TimestampSearchResult.underestimatedResult(
|
||||
lastPcrTimeUsInRange, endOfLastPacketPositionInStream);
|
||||
} else {
|
||||
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -108,6 +108,14 @@ import java.io.IOException;
|
||||
return durationUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the
|
||||
* input TS stream.
|
||||
*/
|
||||
public TimestampAdjuster getPcrTimestampAdjuster() {
|
||||
return pcrTimestampAdjuster;
|
||||
}
|
||||
|
||||
private int finishReadDuration(ExtractorInput input) {
|
||||
isDurationRead = true;
|
||||
input.resetPeekPosition();
|
||||
@ -141,7 +149,7 @@ import java.io.IOException;
|
||||
if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
|
||||
continue;
|
||||
}
|
||||
long pcrValue = readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
|
||||
long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
|
||||
if (pcrValue != C.TIME_UNSET) {
|
||||
return pcrValue;
|
||||
}
|
||||
@ -177,7 +185,7 @@ import java.io.IOException;
|
||||
if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
|
||||
continue;
|
||||
}
|
||||
long pcrValue = readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
|
||||
long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
|
||||
if (pcrValue != C.TIME_UNSET) {
|
||||
return pcrValue;
|
||||
}
|
||||
@ -185,51 +193,4 @@ import java.io.IOException;
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private static long readPcrFromPacket(
|
||||
ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {
|
||||
packetBuffer.setPosition(startOfPacket);
|
||||
if (packetBuffer.bytesLeft() < 5) {
|
||||
// Header = 4 bytes, adaptationFieldLength = 1 byte.
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
|
||||
int tsPacketHeader = packetBuffer.readInt();
|
||||
if ((tsPacketHeader & 0x800000) != 0) {
|
||||
// transport_error_indicator != 0 means there are uncorrectable errors in this packet.
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
|
||||
if (pid != pcrPid) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;
|
||||
if (!adaptationFieldExists) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
int adaptationFieldLength = packetBuffer.readUnsignedByte();
|
||||
if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {
|
||||
int flags = packetBuffer.readUnsignedByte();
|
||||
boolean pcrFlagSet = (flags & 0x10) == 0x10;
|
||||
if (pcrFlagSet) {
|
||||
byte[] pcrBytes = new byte[6];
|
||||
packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);
|
||||
return readPcrValueFromPcrBytes(pcrBytes);
|
||||
}
|
||||
}
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes.
|
||||
*
|
||||
* <p>We ignore PCR Ext, because it's too small to have any significance.
|
||||
*/
|
||||
private static long readPcrValueFromPcrBytes(byte[] pcrBytes) {
|
||||
return (pcrBytes[0] & 0xFFL) << 25
|
||||
| (pcrBytes[1] & 0xFFL) << 17
|
||||
| (pcrBytes[2] & 0xFFL) << 9
|
||||
| (pcrBytes[3] & 0xFFL) << 1
|
||||
| (pcrBytes[4] & 0xFFL) >> 7;
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ public final class TsExtractor implements Extractor {
|
||||
private final TsDurationReader durationReader;
|
||||
|
||||
// Accessed only by the loading thread.
|
||||
private TsBinarySearchSeeker tsBinarySearchSeeker;
|
||||
private ExtractorOutput output;
|
||||
private int remainingPmts;
|
||||
private boolean tracksEnded;
|
||||
@ -208,7 +209,23 @@ public final class TsExtractor implements Extractor {
|
||||
Assertions.checkState(mode != MODE_HLS);
|
||||
int timestampAdjustersCount = timestampAdjusters.size();
|
||||
for (int i = 0; i < timestampAdjustersCount; i++) {
|
||||
timestampAdjusters.get(i).reset();
|
||||
TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i);
|
||||
boolean hasNotEncounteredFirstTimestamp =
|
||||
timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;
|
||||
if (hasNotEncounteredFirstTimestamp
|
||||
|| (timestampAdjuster.getTimestampOffsetUs() != 0
|
||||
&& timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {
|
||||
// - If a track in the TS stream has not encountered any sample, it's going to treat the
|
||||
// first sample encountered as timestamp 0, which is incorrect. So we have to set the first
|
||||
// sample timestamp for that track manually.
|
||||
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
|
||||
// different position, we need to set the first sample timestamp manually again.
|
||||
timestampAdjuster.reset();
|
||||
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
|
||||
}
|
||||
}
|
||||
if (timeUs != 0 && tsBinarySearchSeeker != null) {
|
||||
tsBinarySearchSeeker.setSeekTargetUs(timeUs);
|
||||
}
|
||||
tsPacketBuffer.reset();
|
||||
continuityCounters.clear();
|
||||
@ -227,11 +244,12 @@ public final class TsExtractor implements Extractor {
|
||||
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
if (tracksEnded) {
|
||||
boolean canReadDuration = input.getLength() != C.LENGTH_UNSET && mode != MODE_HLS;
|
||||
long inputLength = input.getLength();
|
||||
boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;
|
||||
if (canReadDuration && !durationReader.isDurationReadFinished()) {
|
||||
return durationReader.readDuration(input, seekPosition, pcrPid);
|
||||
}
|
||||
maybeOutputSeekMap();
|
||||
maybeOutputSeekMap(inputLength);
|
||||
|
||||
if (pendingSeekToStart) {
|
||||
pendingSeekToStart = false;
|
||||
@ -241,6 +259,11 @@ public final class TsExtractor implements Extractor {
|
||||
return RESULT_SEEK;
|
||||
}
|
||||
}
|
||||
|
||||
if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {
|
||||
return tsBinarySearchSeeker.handlePendingSeek(
|
||||
input, seekPosition, /* outputFrameHolder= */ null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fillBufferWithAtLeastOnePacket(input)) {
|
||||
@ -314,12 +337,22 @@ public final class TsExtractor implements Extractor {
|
||||
|
||||
// Internals.
|
||||
|
||||
private void maybeOutputSeekMap() {
|
||||
private void maybeOutputSeekMap(long inputLength) {
|
||||
if (!hasOutputSeekMap) {
|
||||
hasOutputSeekMap = true;
|
||||
if (durationReader.getDurationUs() != C.TIME_UNSET) {
|
||||
tsBinarySearchSeeker =
|
||||
new TsBinarySearchSeeker(
|
||||
durationReader.getPcrTimestampAdjuster(),
|
||||
durationReader.getDurationUs(),
|
||||
inputLength,
|
||||
pcrPid);
|
||||
output.seekMap(tsBinarySearchSeeker.getSeekMap());
|
||||
} else {
|
||||
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
@ -353,7 +386,7 @@ public final class TsExtractor implements Extractor {
|
||||
private int findEndOfFirstTsPacketInBuffer() throws ParserException {
|
||||
int searchStart = tsPacketBuffer.getPosition();
|
||||
int limit = tsPacketBuffer.limit();
|
||||
int syncBytePosition = findSyncBytePosition(tsPacketBuffer.data, searchStart, limit);
|
||||
int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit);
|
||||
// Discard all bytes before the sync byte.
|
||||
// If sync byte is not found, this means discard the whole buffer.
|
||||
tsPacketBuffer.setPosition(syncBytePosition);
|
||||
@ -370,18 +403,6 @@ public final class TsExtractor implements Extractor {
|
||||
return endOfPacket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition)
|
||||
* from the provided data array, or returns limitPosition if sync byte could not be found.
|
||||
*/
|
||||
private static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) {
|
||||
int position = startPosition;
|
||||
while (position < limitPosition && data[position] != TS_SYNC_BYTE) {
|
||||
position++;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
private boolean shouldConsumePacketPayload(int packetPid) {
|
||||
return mode == MODE_HLS
|
||||
|| tracksEnded
|
||||
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
||||
/** Utilities method for extracting MPEG-TS streams. */
|
||||
public final class TsUtil {
|
||||
/**
|
||||
* Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition)
|
||||
* from the provided data array, or returns limitPosition if sync byte could not be found.
|
||||
*/
|
||||
public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) {
|
||||
int position = startPosition;
|
||||
while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) {
|
||||
position++;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PCR value read from a given TS packet.
|
||||
*
|
||||
* @param packetBuffer The buffer that holds the packet.
|
||||
* @param startOfPacket The starting position of the packet in the buffer.
|
||||
* @param pcrPid The PID for valid packets that contain PCR values.
|
||||
* @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it
|
||||
* contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise.
|
||||
*/
|
||||
public static long readPcrFromPacket(
|
||||
ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {
|
||||
packetBuffer.setPosition(startOfPacket);
|
||||
if (packetBuffer.bytesLeft() < 5) {
|
||||
// Header = 4 bytes, adaptationFieldLength = 1 byte.
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
|
||||
int tsPacketHeader = packetBuffer.readInt();
|
||||
if ((tsPacketHeader & 0x800000) != 0) {
|
||||
// transport_error_indicator != 0 means there are uncorrectable errors in this packet.
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
|
||||
if (pid != pcrPid) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;
|
||||
if (!adaptationFieldExists) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
int adaptationFieldLength = packetBuffer.readUnsignedByte();
|
||||
if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {
|
||||
int flags = packetBuffer.readUnsignedByte();
|
||||
boolean pcrFlagSet = (flags & 0x10) == 0x10;
|
||||
if (pcrFlagSet) {
|
||||
byte[] pcrBytes = new byte[6];
|
||||
packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);
|
||||
return readPcrValueFromPcrBytes(pcrBytes);
|
||||
}
|
||||
}
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes.
|
||||
*
|
||||
* <p>We ignore PCR Ext, because it's too small to have any significance.
|
||||
*/
|
||||
private static long readPcrValueFromPcrBytes(byte[] pcrBytes) {
|
||||
return (pcrBytes[0] & 0xFFL) << 25
|
||||
| (pcrBytes[1] & 0xFFL) << 17
|
||||
| (pcrBytes[2] & 0xFFL) << 9
|
||||
| (pcrBytes[3] & 0xFFL) << 1
|
||||
| (pcrBytes[4] & 0xFFL) >> 7;
|
||||
}
|
||||
|
||||
private TsUtil() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
seekMap:
|
||||
isSeekable = false
|
||||
isSeekable = true
|
||||
duration = 66733
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 3
|
||||
|
99
library/core/src/test/assets/ts/sample.ts.1.dump
Normal file
99
library/core/src/test/assets/ts/sample.ts.1.dump
Normal file
@ -0,0 +1,99 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 66733
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 3
|
||||
track 256:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/256
|
||||
containerMimeType = null
|
||||
sampleMimeType = video/mpeg2
|
||||
maxInputSize = -1
|
||||
width = 640
|
||||
height = 426
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 22, hash CE183139
|
||||
total output bytes = 24315
|
||||
sample count = 1
|
||||
sample 0:
|
||||
time = 55611
|
||||
flags = 0
|
||||
data = length 18112, hash EC44B35B
|
||||
track 257:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/257
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mpeg-L2
|
||||
maxInputSize = 4096
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 1
|
||||
sampleRate = 44100
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 5015
|
||||
sample count = 4
|
||||
sample 0:
|
||||
time = 11333
|
||||
flags = 1
|
||||
data = length 1253, hash 727FD1C6
|
||||
sample 1:
|
||||
time = 37455
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
sample 2:
|
||||
time = 63578
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
sample 3:
|
||||
time = 89700
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
track 8448:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/8448
|
||||
containerMimeType = null
|
||||
sampleMimeType = application/cea-608
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
tracksEnded = true
|
99
library/core/src/test/assets/ts/sample.ts.2.dump
Normal file
99
library/core/src/test/assets/ts/sample.ts.2.dump
Normal file
@ -0,0 +1,99 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 66733
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 3
|
||||
track 256:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/256
|
||||
containerMimeType = null
|
||||
sampleMimeType = video/mpeg2
|
||||
maxInputSize = -1
|
||||
width = 640
|
||||
height = 426
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 22, hash CE183139
|
||||
total output bytes = 24315
|
||||
sample count = 1
|
||||
sample 0:
|
||||
time = 77855
|
||||
flags = 0
|
||||
data = length 18112, hash EC44B35B
|
||||
track 257:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/257
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mpeg-L2
|
||||
maxInputSize = 4096
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 1
|
||||
sampleRate = 44100
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 5015
|
||||
sample count = 4
|
||||
sample 0:
|
||||
time = 33577
|
||||
flags = 1
|
||||
data = length 1253, hash 727FD1C6
|
||||
sample 1:
|
||||
time = 59699
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
sample 2:
|
||||
time = 85822
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
sample 3:
|
||||
time = 111944
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
track 8448:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/8448
|
||||
containerMimeType = null
|
||||
sampleMimeType = application/cea-608
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
tracksEnded = true
|
87
library/core/src/test/assets/ts/sample.ts.3.dump
Normal file
87
library/core/src/test/assets/ts/sample.ts.3.dump
Normal file
@ -0,0 +1,87 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 66733
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 3
|
||||
track 256:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/256
|
||||
containerMimeType = null
|
||||
sampleMimeType = video/mpeg2
|
||||
maxInputSize = -1
|
||||
width = 640
|
||||
height = 426
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 22, hash CE183139
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
track 257:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/257
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mpeg-L2
|
||||
maxInputSize = 4096
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 1
|
||||
sampleRate = 44100
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 2508
|
||||
sample count = 2
|
||||
sample 0:
|
||||
time = 66733
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
sample 1:
|
||||
time = 92855
|
||||
flags = 1
|
||||
data = length 1254, hash 73FB07B8
|
||||
track 8448:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1/8448
|
||||
containerMimeType = null
|
||||
sampleMimeType = application/cea-608
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
tracksEnded = true
|
@ -90,7 +90,7 @@ public final class AdtsExtractorSeekTest {
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long targetSeekTimeUs = 3330033; // 980_000;
|
||||
long targetSeekTimeUs = 980_000;
|
||||
int extractedSampleIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
/** Seeking tests for {@link TsExtractor}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class TsExtractorSeekTest {
|
||||
|
||||
private static final String TEST_FILE = "ts/bbb_2500ms.ts";
|
||||
private static final int DURATION_US = 2_500_000;
|
||||
private static final int AUDIO_TRACK_ID = 257;
|
||||
private static final long MAXIMUM_TIMESTAMP_DELTA_US = 500_000L;
|
||||
|
||||
private static final Random random = new Random(1234L);
|
||||
|
||||
private FakeTrackOutput expectedTrackOutput;
|
||||
private DefaultDataSource dataSource;
|
||||
private PositionHolder positionHolder;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException, InterruptedException {
|
||||
positionHolder = new PositionHolder();
|
||||
expectedTrackOutput =
|
||||
TestUtil.extractAllSamplesFromFile(
|
||||
new TsExtractor(), RuntimeEnvironment.application, TEST_FILE)
|
||||
.trackOutputs
|
||||
.get(AUDIO_TRACK_ID);
|
||||
|
||||
dataSource =
|
||||
new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent")
|
||||
.createDataSource();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
|
||||
throws IOException, InterruptedException {
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
|
||||
SeekMap seekMap =
|
||||
TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri);
|
||||
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);
|
||||
|
||||
long targetSeekTimeUs = 987_000;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
|
||||
throws IOException, InterruptedException {
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);
|
||||
|
||||
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
long targetSeekTimeUs = 0;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
long targetSeekTimeUs = 1_234_000;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);
|
||||
|
||||
long numSeek = 100;
|
||||
for (long i = 0; i < numSeek; i++) {
|
||||
long targetSeekTimeUs = random.nextInt(DURATION_US + 1);
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
TsExtractor extractor = new TsExtractor();
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
readInputFileOnce(extractor, extractorOutput, fileUri);
|
||||
SeekMap seekMap = extractorOutput.seekMap;
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);
|
||||
|
||||
long numSeek = 100;
|
||||
for (long i = 0; i < numSeek; i++) {
|
||||
long targetSeekTimeUs = random.nextInt(DURATION_US + 1);
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private void readInputFileOnce(
|
||||
TsExtractor extractor, FakeExtractorOutput extractorOutput, Uri fileUri)
|
||||
throws IOException, InterruptedException {
|
||||
extractor.init(extractorOutput);
|
||||
int readResult = Extractor.RESULT_CONTINUE;
|
||||
ExtractorInput input = TestUtil.getExtractorInputFromPosition(dataSource, 0, fileUri);
|
||||
while (readResult != Extractor.RESULT_END_OF_INPUT) {
|
||||
try {
|
||||
while (readResult == Extractor.RESULT_CONTINUE) {
|
||||
readResult = extractor.read(input, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
if (readResult == Extractor.RESULT_SEEK) {
|
||||
input =
|
||||
TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, fileUri);
|
||||
readResult = Extractor.RESULT_CONTINUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {
|
||||
long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek);
|
||||
int expectedSampleIndex =
|
||||
findOutputFrameInExpectedOutput(trackOutput.getSampleData(firstFrameIndexAfterSeek));
|
||||
// Assert that after seeking, the first sample frame written to output exists in the sample list
|
||||
assertThat(expectedSampleIndex).isNotEqualTo(-1);
|
||||
// Assert that the timestamp output for first sample after seek is near the seek point.
|
||||
// For Ts seeking, unfortunately we can't guarantee exact frame seeking, since PID timestamp is
|
||||
// not too reliable.
|
||||
assertThat(Math.abs(outputSampleTimeUs - seekTimeUs)).isLessThan(MAXIMUM_TIMESTAMP_DELTA_US);
|
||||
// Assert that the timestamp output for first sample after seek is near the actual sample
|
||||
// at seek point.
|
||||
// Note that the timestamp output for first sample after seek might *NOT* be equal to the
|
||||
// timestamp of that same sample when reading from the beginning, because if first timestamp in
|
||||
// the stream was not read before the seek, then the timestamp of the first sample after the
|
||||
// seek is just approximated from the seek point.
|
||||
assertThat(
|
||||
Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex)))
|
||||
.isLessThan(MAXIMUM_TIMESTAMP_DELTA_US);
|
||||
trackOutput.assertSample(
|
||||
firstFrameIndexAfterSeek,
|
||||
expectedTrackOutput.getSampleData(expectedSampleIndex),
|
||||
outputSampleTimeUs,
|
||||
expectedTrackOutput.getSampleFlags(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));
|
||||
}
|
||||
|
||||
private int findOutputFrameInExpectedOutput(byte[] sampleData) {
|
||||
for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) {
|
||||
byte[] currentSampleData = expectedTrackOutput.getSampleData(i);
|
||||
if (Arrays.equals(currentSampleData, sampleData)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -61,15 +61,21 @@ public final class TsExtractorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompleteSample() throws Exception {
|
||||
public void testStreamWithJunkData() throws Exception {
|
||||
Random random = new Random(0);
|
||||
byte[] fileData = TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ts");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2);
|
||||
int bytesLeft = fileData.length;
|
||||
|
||||
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
|
||||
out.write(fileData, 0, TS_PACKET_SIZE * 5);
|
||||
for (int i = TS_PACKET_SIZE * 5; i < fileData.length; i += TS_PACKET_SIZE) {
|
||||
bytesLeft -= TS_PACKET_SIZE * 5;
|
||||
|
||||
for (int i = TS_PACKET_SIZE * 5; i < fileData.length; i += 5 * TS_PACKET_SIZE) {
|
||||
writeJunkData(out, random.nextInt(TS_PACKET_SIZE));
|
||||
out.write(fileData, i, TS_PACKET_SIZE);
|
||||
int length = Math.min(5 * TS_PACKET_SIZE, bytesLeft);
|
||||
out.write(fileData, i, length);
|
||||
bytesLeft -= length;
|
||||
}
|
||||
out.write(TS_SYNC_BYTE);
|
||||
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
|
||||
|
@ -368,10 +368,13 @@ public class TestUtil {
|
||||
}
|
||||
|
||||
/** Returns an {@link ExtractorInput} to read from the given input at given position. */
|
||||
private static ExtractorInput getExtractorInputFromPosition(
|
||||
public static ExtractorInput getExtractorInputFromPosition(
|
||||
DataSource dataSource, long position, Uri uri) throws IOException {
|
||||
DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, /* key= */ null);
|
||||
long inputLength = dataSource.open(dataSpec);
|
||||
return new DefaultExtractorInput(dataSource, position, inputLength);
|
||||
long length = dataSource.open(dataSpec);
|
||||
if (length != C.LENGTH_UNSET) {
|
||||
length += position;
|
||||
}
|
||||
return new DefaultExtractorInput(dataSource, position, length);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user