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:
hoangtc 2018-08-06 05:23:10 -07:00 committed by Oliver Woodman
parent 7fcd6b6d65
commit 077f2c3036
13 changed files with 867 additions and 77 deletions

View File

@ -4,8 +4,11 @@
* Add `AudioListener` for listening to changes in audio configuration during * Add `AudioListener` for listening to changes in audio configuration during
playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).
* MPEG-TS: Support CEA-608/708 in H262 * MPEG-TS:
([#2565](https://github.com/google/ExoPlayer/issues/2565)). * 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 * MPEG-PS: Support reading duration and seeking for MPEG-PS Streams
([#4476](https://github.com/google/ExoPlayer/issues/4476)). ([#4476](https://github.com/google/ExoPlayer/issues/4476)).
* MediaSession extension: * MediaSession extension:

View File

@ -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;
}
}
}
}

View File

@ -108,6 +108,14 @@ import java.io.IOException;
return durationUs; 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) { private int finishReadDuration(ExtractorInput input) {
isDurationRead = true; isDurationRead = true;
input.resetPeekPosition(); input.resetPeekPosition();
@ -141,7 +149,7 @@ import java.io.IOException;
if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
continue; continue;
} }
long pcrValue = readPcrFromPacket(packetBuffer, searchPosition, pcrPid); long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
if (pcrValue != C.TIME_UNSET) { if (pcrValue != C.TIME_UNSET) {
return pcrValue; return pcrValue;
} }
@ -177,7 +185,7 @@ import java.io.IOException;
if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
continue; continue;
} }
long pcrValue = readPcrFromPacket(packetBuffer, searchPosition, pcrPid); long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
if (pcrValue != C.TIME_UNSET) { if (pcrValue != C.TIME_UNSET) {
return pcrValue; return pcrValue;
} }
@ -185,51 +193,4 @@ import java.io.IOException;
return C.TIME_UNSET; 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;
}
} }

View File

@ -113,6 +113,7 @@ public final class TsExtractor implements Extractor {
private final TsDurationReader durationReader; private final TsDurationReader durationReader;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private TsBinarySearchSeeker tsBinarySearchSeeker;
private ExtractorOutput output; private ExtractorOutput output;
private int remainingPmts; private int remainingPmts;
private boolean tracksEnded; private boolean tracksEnded;
@ -208,7 +209,23 @@ public final class TsExtractor implements Extractor {
Assertions.checkState(mode != MODE_HLS); Assertions.checkState(mode != MODE_HLS);
int timestampAdjustersCount = timestampAdjusters.size(); int timestampAdjustersCount = timestampAdjusters.size();
for (int i = 0; i < timestampAdjustersCount; i++) { 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(); tsPacketBuffer.reset();
continuityCounters.clear(); continuityCounters.clear();
@ -227,11 +244,12 @@ public final class TsExtractor implements Extractor {
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (tracksEnded) { 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()) { if (canReadDuration && !durationReader.isDurationReadFinished()) {
return durationReader.readDuration(input, seekPosition, pcrPid); return durationReader.readDuration(input, seekPosition, pcrPid);
} }
maybeOutputSeekMap(); maybeOutputSeekMap(inputLength);
if (pendingSeekToStart) { if (pendingSeekToStart) {
pendingSeekToStart = false; pendingSeekToStart = false;
@ -241,6 +259,11 @@ public final class TsExtractor implements Extractor {
return RESULT_SEEK; return RESULT_SEEK;
} }
} }
if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {
return tsBinarySearchSeeker.handlePendingSeek(
input, seekPosition, /* outputFrameHolder= */ null);
}
} }
if (!fillBufferWithAtLeastOnePacket(input)) { if (!fillBufferWithAtLeastOnePacket(input)) {
@ -314,10 +337,20 @@ public final class TsExtractor implements Extractor {
// Internals. // Internals.
private void maybeOutputSeekMap() { private void maybeOutputSeekMap(long inputLength) {
if (!hasOutputSeekMap) { if (!hasOutputSeekMap) {
hasOutputSeekMap = true; hasOutputSeekMap = true;
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); 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()));
}
} }
} }
@ -353,7 +386,7 @@ public final class TsExtractor implements Extractor {
private int findEndOfFirstTsPacketInBuffer() throws ParserException { private int findEndOfFirstTsPacketInBuffer() throws ParserException {
int searchStart = tsPacketBuffer.getPosition(); int searchStart = tsPacketBuffer.getPosition();
int limit = tsPacketBuffer.limit(); 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. // Discard all bytes before the sync byte.
// If sync byte is not found, this means discard the whole buffer. // If sync byte is not found, this means discard the whole buffer.
tsPacketBuffer.setPosition(syncBytePosition); tsPacketBuffer.setPosition(syncBytePosition);
@ -370,18 +403,6 @@ public final class TsExtractor implements Extractor {
return endOfPacket; 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) { private boolean shouldConsumePacketPayload(int packetPid) {
return mode == MODE_HLS return mode == MODE_HLS
|| tracksEnded || tracksEnded

View File

@ -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.
}
}

View File

@ -1,5 +1,5 @@
seekMap: seekMap:
isSeekable = false isSeekable = true
duration = 66733 duration = 66733
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 3 numberOfTracks = 3

View 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

View 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

View 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

View File

@ -90,7 +90,7 @@ public final class AdtsExtractorSeekTest {
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = 3330033; // 980_000; long targetSeekTimeUs = 980_000;
int extractedSampleIndex = int extractedSampleIndex =
TestUtil.seekToTimeUs( TestUtil.seekToTimeUs(
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);

View File

@ -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;
}
}

View File

@ -61,15 +61,21 @@ public final class TsExtractorTest {
} }
@Test @Test
public void testIncompleteSample() throws Exception { public void testStreamWithJunkData() throws Exception {
Random random = new Random(0); Random random = new Random(0);
byte[] fileData = TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ts"); byte[] fileData = TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ts");
ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2); ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2);
int bytesLeft = fileData.length;
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
out.write(fileData, 0, TS_PACKET_SIZE * 5); 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)); 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); out.write(TS_SYNC_BYTE);
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);

View File

@ -368,10 +368,13 @@ public class TestUtil {
} }
/** Returns an {@link ExtractorInput} to read from the given input at given position. */ /** 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 { DataSource dataSource, long position, Uri uri) throws IOException {
DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, /* key= */ null); DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, /* key= */ null);
long inputLength = dataSource.open(dataSpec); long length = dataSource.open(dataSpec);
return new DefaultExtractorInput(dataSource, position, inputLength); if (length != C.LENGTH_UNSET) {
length += position;
}
return new DefaultExtractorInput(dataSource, position, length);
} }
} }