mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Supports seeking for MPEG PS Streams.
This CL adds support for seeking within PS streams by using binary search. For any seek timestamp, it tries to find the location in the stream where SCR timestamp is close to the target timestamp, and return this position as the seek position. Github: #4476. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=206787691
This commit is contained in:
parent
377314a69f
commit
f08ad55892
@ -9,7 +9,7 @@
|
||||
([#2565](https://github.com/google/ExoPlayer/issues/2565)).
|
||||
* Fix bug preventing SCTE-35 cues from being output
|
||||
([#4573](https://github.com/google/ExoPlayer/issues/4573)).
|
||||
* MPEG-PS: Support reading duration from MPEG-PS Streams
|
||||
* MPEG-PS: Support reading duration and seeking for MPEG-PS Streams
|
||||
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
|
||||
* MediaSession extension:
|
||||
* Allow apps to set custom errors.
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
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.extractor.SeekMap;
|
||||
@ -75,7 +74,6 @@ import java.nio.ByteBuffer;
|
||||
throws IOException, InterruptedException {
|
||||
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
|
||||
long searchPosition = input.getPosition();
|
||||
int searchRangeBytes = getTimestampSearchBytesRange();
|
||||
decoderJni.reset(searchPosition);
|
||||
try {
|
||||
decoderJni.decodeSampleWithBacktrackPosition(
|
||||
@ -107,13 +105,6 @@ import java.nio.ByteBuffer;
|
||||
return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimestampSearchBytesRange() {
|
||||
// We rely on decoderJni to search for timestamp (sample index) from a given stream point, so
|
||||
// we don't restrict the range at all.
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,14 +72,6 @@ public abstract class BinarySearchSeeker {
|
||||
TimestampSearchResult searchForTimestamp(
|
||||
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
/**
|
||||
* The range of bytes from the current input position from which to search for the target
|
||||
* timestamp. Uses {@link C#LENGTH_UNSET} to signal that there is no limit for the search range.
|
||||
*
|
||||
* @see #searchForTimestamp(ExtractorInput, long, OutputFrameHolder)
|
||||
*/
|
||||
int getTimestampSearchBytesRange();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,6 +90,18 @@ public abstract class BinarySearchSeeker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
|
||||
* timestamp for a seek time position.
|
||||
*/
|
||||
public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {
|
||||
|
||||
@Override
|
||||
public long timeUsToTargetTime(long timeUs) {
|
||||
return timeUs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A converter that converts seek time in stream time into target timestamp for the {@link
|
||||
* BinarySearchSeeker}.
|
||||
@ -566,16 +570,4 @@ public abstract class BinarySearchSeeker {
|
||||
return seekTimestampConverter.timeUsToTargetTime(timeUs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
|
||||
* timestamp for a seek time position.
|
||||
*/
|
||||
private static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {
|
||||
|
||||
@Override
|
||||
public long timeUsToTargetTime(long timeUs) {
|
||||
return timeUs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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 PS stream using binary search.
|
||||
*
|
||||
* <p>This seeker uses the first and last SCR values within the stream, as well as the stream
|
||||
* duration to interpolate the SCR value of the seeking position. Then it performs binary search
|
||||
* within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from
|
||||
* the target SCR.
|
||||
*/
|
||||
/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker {
|
||||
|
||||
private static final long SEEK_TOLERANCE_US = 100_000;
|
||||
private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000;
|
||||
private static final int TIMESTAMP_SEARCH_BYTES = 20000;
|
||||
|
||||
public PsBinarySearchSeeker(
|
||||
TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) {
|
||||
super(
|
||||
new DefaultSeekTimestampConverter(),
|
||||
new PsScrSeeker(scrTimestampAdjuster),
|
||||
streamDurationUs,
|
||||
/* floorTimePosition= */ 0,
|
||||
/* ceilingTimePosition= */ streamDurationUs + 1,
|
||||
/* floorBytePosition= */ 0,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
|
||||
MINIMUM_SEARCH_RANGE_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* A seeker that looks for a given SCR timestamp at a given position in a PS stream.
|
||||
*
|
||||
* <p>Given a SCR timestamp, and a position within a PS stream, this seeker will try to read a
|
||||
* range of up to {@link #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all
|
||||
* packs in that range, and then compare the SCR timestamps (if available) of these packets vs the
|
||||
* target timestamp.
|
||||
*/
|
||||
private static final class PsScrSeeker implements TimestampSeeker {
|
||||
|
||||
private final TimestampAdjuster scrTimestampAdjuster;
|
||||
private final ParsableByteArray packetBuffer;
|
||||
|
||||
private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
|
||||
this.scrTimestampAdjuster = scrTimestampAdjuster;
|
||||
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 searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
|
||||
}
|
||||
|
||||
private TimestampSearchResult searchForScrValueInBuffer(
|
||||
ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
|
||||
int startOfLastPacketPosition = C.POSITION_UNSET;
|
||||
int endOfLastPacketPosition = C.POSITION_UNSET;
|
||||
long lastScrTimeUsInRange = C.TIME_UNSET;
|
||||
|
||||
while (packetBuffer.bytesLeft() >= 4) {
|
||||
int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
|
||||
if (nextStartCode != PsExtractor.PACK_START_CODE) {
|
||||
packetBuffer.skipBytes(1);
|
||||
continue;
|
||||
} else {
|
||||
packetBuffer.skipBytes(4);
|
||||
}
|
||||
|
||||
// We found a pack.
|
||||
long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer);
|
||||
if (scrValue != C.TIME_UNSET) {
|
||||
long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue);
|
||||
if (scrTimeUs > targetScrTimeUs) {
|
||||
if (lastScrTimeUsInRange == C.TIME_UNSET) {
|
||||
// First SCR timestamp is already over target.
|
||||
return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset);
|
||||
} else {
|
||||
// Last SCR timestamp < target timestamp < this timestamp.
|
||||
return TimestampSearchResult.targetFoundResult(
|
||||
bufferStartOffset + startOfLastPacketPosition);
|
||||
}
|
||||
} else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) {
|
||||
long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition();
|
||||
return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
|
||||
}
|
||||
|
||||
lastScrTimeUsInRange = scrTimeUs;
|
||||
startOfLastPacketPosition = packetBuffer.getPosition();
|
||||
}
|
||||
skipToEndOfCurrentPack(packetBuffer);
|
||||
endOfLastPacketPosition = packetBuffer.getPosition();
|
||||
}
|
||||
|
||||
if (lastScrTimeUsInRange != C.TIME_UNSET) {
|
||||
long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
|
||||
return TimestampSearchResult.underestimatedResult(
|
||||
lastScrTimeUsInRange, endOfLastPacketPositionInStream);
|
||||
} else {
|
||||
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the buffer position to the position after the end of the current PS pack in the buffer,
|
||||
* given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in
|
||||
* the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer.
|
||||
*/
|
||||
private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) {
|
||||
int limit = packetBuffer.limit();
|
||||
|
||||
if (packetBuffer.bytesLeft() < 10) {
|
||||
// We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing
|
||||
// length.
|
||||
packetBuffer.setPosition(limit);
|
||||
return;
|
||||
}
|
||||
packetBuffer.skipBytes(9);
|
||||
|
||||
int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07;
|
||||
if (packetBuffer.bytesLeft() < packStuffingLength) {
|
||||
packetBuffer.setPosition(limit);
|
||||
return;
|
||||
}
|
||||
packetBuffer.skipBytes(packStuffingLength);
|
||||
|
||||
if (packetBuffer.bytesLeft() < 4) {
|
||||
packetBuffer.setPosition(limit);
|
||||
return;
|
||||
}
|
||||
|
||||
int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
|
||||
if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) {
|
||||
packetBuffer.skipBytes(4);
|
||||
int systemHeaderLength = packetBuffer.readUnsignedShort();
|
||||
if (packetBuffer.bytesLeft() < systemHeaderLength) {
|
||||
packetBuffer.setPosition(limit);
|
||||
return;
|
||||
}
|
||||
packetBuffer.skipBytes(systemHeaderLength);
|
||||
}
|
||||
|
||||
// Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right
|
||||
// after the end position of this pack.
|
||||
// If we couldn't find these codes within the buffer, return the buffer limit, or return
|
||||
// the first position which PES packets pattern does not match (some malformed packets).
|
||||
while (packetBuffer.bytesLeft() >= 4) {
|
||||
nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
|
||||
if (nextStartCode == PsExtractor.PACK_START_CODE
|
||||
|| nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) {
|
||||
break;
|
||||
}
|
||||
if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) {
|
||||
break;
|
||||
}
|
||||
packetBuffer.skipBytes(4);
|
||||
|
||||
if (packetBuffer.bytesLeft() < 2) {
|
||||
// 2 bytes for PES_packet length.
|
||||
packetBuffer.setPosition(limit);
|
||||
return;
|
||||
}
|
||||
int pesPacketLength = packetBuffer.readUnsignedShort();
|
||||
packetBuffer.setPosition(
|
||||
Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int peekIntAtPosition(byte[] data, int position) {
|
||||
return (data[position] & 0xFF) << 24
|
||||
| (data[position + 1] & 0xFF) << 16
|
||||
| (data[position + 2] & 0xFF) << 8
|
||||
| (data[position + 3] & 0xFF);
|
||||
}
|
||||
}
|
@ -64,6 +64,10 @@ import java.io.IOException;
|
||||
return isDurationRead;
|
||||
}
|
||||
|
||||
public TimestampAdjuster getScrTimestampAdjuster() {
|
||||
return scrTimestampAdjuster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a PS duration from the input.
|
||||
*
|
||||
@ -105,6 +109,25 @@ import java.io.IOException;
|
||||
return durationUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SCR value read from the next pack in the stream, given the buffer at the pack
|
||||
* header start position (just behind the pack start code).
|
||||
*/
|
||||
public static long readScrValueFromPack(ParsableByteArray packetBuffer) {
|
||||
int originalPosition = packetBuffer.getPosition();
|
||||
if (packetBuffer.bytesLeft() < 9) {
|
||||
// We require at 9 bytes for pack header to read scr value
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
byte[] scrBytes = new byte[9];
|
||||
packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length);
|
||||
packetBuffer.setPosition(originalPosition);
|
||||
if (!checkMarkerBits(scrBytes)) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
return readScrValueFromPackHeader(scrBytes);
|
||||
}
|
||||
|
||||
private int finishReadDuration(ExtractorInput input) {
|
||||
isDurationRead = true;
|
||||
input.resetPeekPosition();
|
||||
@ -135,9 +158,10 @@ import java.io.IOException;
|
||||
for (int searchPosition = searchStartPosition;
|
||||
searchPosition < searchEndPosition - 3;
|
||||
searchPosition++) {
|
||||
int nextStartCode = peakIntAtPosition(packetBuffer.data, searchPosition);
|
||||
int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);
|
||||
if (nextStartCode == PsExtractor.PACK_START_CODE) {
|
||||
long scrValue = readScrValueFromPack(packetBuffer, searchPosition + 4);
|
||||
packetBuffer.setPosition(searchPosition + 4);
|
||||
long scrValue = readScrValueFromPack(packetBuffer);
|
||||
if (scrValue != C.TIME_UNSET) {
|
||||
return scrValue;
|
||||
}
|
||||
@ -171,9 +195,10 @@ import java.io.IOException;
|
||||
for (int searchPosition = searchEndPosition - 4;
|
||||
searchPosition >= searchStartPosition;
|
||||
searchPosition--) {
|
||||
int nextStartCode = peakIntAtPosition(packetBuffer.data, searchPosition);
|
||||
int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);
|
||||
if (nextStartCode == PsExtractor.PACK_START_CODE) {
|
||||
long scrValue = readScrValueFromPack(packetBuffer, searchPosition + 4);
|
||||
packetBuffer.setPosition(searchPosition + 4);
|
||||
long scrValue = readScrValueFromPack(packetBuffer);
|
||||
if (scrValue != C.TIME_UNSET) {
|
||||
return scrValue;
|
||||
}
|
||||
@ -182,28 +207,14 @@ import java.io.IOException;
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private int peakIntAtPosition(byte[] data, int position) {
|
||||
private int peekIntAtPosition(byte[] data, int position) {
|
||||
return (data[position] & 0xFF) << 24
|
||||
| (data[position + 1] & 0xFF) << 16
|
||||
| (data[position + 2] & 0xFF) << 8
|
||||
| (data[position + 3] & 0xFF);
|
||||
}
|
||||
|
||||
private long readScrValueFromPack(ParsableByteArray packetBuffer, int packHeaderStartPosition) {
|
||||
packetBuffer.setPosition(packHeaderStartPosition);
|
||||
if (packetBuffer.bytesLeft() < 9) {
|
||||
// We require at 9 bytes for pack header to read scr value
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
byte[] scrBytes = new byte[9];
|
||||
packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length);
|
||||
if (!checkMarkerBits(scrBytes)) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
return readScrValueFromPackHeader(scrBytes);
|
||||
}
|
||||
|
||||
private boolean checkMarkerBits(byte[] scrBytes) {
|
||||
private static boolean checkMarkerBits(byte[] scrBytes) {
|
||||
// Verify the 01xxx1xx marker on the 0th byte
|
||||
if ((scrBytes[0] & 0xC4) != 0x44) {
|
||||
return false;
|
||||
|
@ -39,9 +39,9 @@ public final class PsExtractor implements Extractor {
|
||||
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()};
|
||||
|
||||
/* package */ static final int PACK_START_CODE = 0x000001BA;
|
||||
private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
|
||||
private static final int PACKET_START_CODE_PREFIX = 0x000001;
|
||||
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
|
||||
/* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
|
||||
/* package */ static final int PACKET_START_CODE_PREFIX = 0x000001;
|
||||
/* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
|
||||
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
|
||||
|
||||
// Max search length for first audio and video track in input data.
|
||||
@ -67,6 +67,7 @@ public final class PsExtractor implements Extractor {
|
||||
private long lastTrackPosition;
|
||||
|
||||
// Accessed only by the loading thread.
|
||||
private PsBinarySearchSeeker psBinarySearchSeeker;
|
||||
private ExtractorOutput output;
|
||||
private boolean hasOutputSeekMap;
|
||||
|
||||
@ -129,7 +130,23 @@ public final class PsExtractor implements Extractor {
|
||||
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
boolean hasNotEncounteredFirstTimestamp =
|
||||
timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;
|
||||
if (hasNotEncounteredFirstTimestamp
|
||||
|| (timestampAdjuster.getFirstSampleTimestampUs() != 0
|
||||
&& timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {
|
||||
// - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to
|
||||
// treat the first timestamp encountered as sample time 0, which is incorrect. In this case,
|
||||
// we have to set the first sample timestamp 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 (psBinarySearchSeeker != null) {
|
||||
psBinarySearchSeeker.setSeekTargetUs(timeUs);
|
||||
}
|
||||
for (int i = 0; i < psPayloadReaders.size(); i++) {
|
||||
psPayloadReaders.valueAt(i).seek();
|
||||
}
|
||||
@ -144,12 +161,23 @@ public final class PsExtractor implements Extractor {
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
boolean canReadDuration = input.getLength() != C.LENGTH_UNSET;
|
||||
long inputLength = input.getLength();
|
||||
boolean canReadDuration = inputLength != C.LENGTH_UNSET;
|
||||
if (canReadDuration && !durationReader.isDurationReadFinished()) {
|
||||
return durationReader.readDuration(input, seekPosition);
|
||||
}
|
||||
maybeOutputSeekMap();
|
||||
maybeOutputSeekMap(inputLength);
|
||||
if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) {
|
||||
return psBinarySearchSeeker.handlePendingSeek(
|
||||
input, seekPosition, /* outputFrameHolder= */ null);
|
||||
}
|
||||
|
||||
input.resetPeekPosition();
|
||||
long peekBytesLeft =
|
||||
inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET;
|
||||
if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
// First peek and check what type of start code is next.
|
||||
if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
@ -251,12 +279,21 @@ public final class PsExtractor implements Extractor {
|
||||
|
||||
// Internals.
|
||||
|
||||
private void maybeOutputSeekMap() {
|
||||
private void maybeOutputSeekMap(long inputLength) {
|
||||
if (!hasOutputSeekMap) {
|
||||
hasOutputSeekMap = true;
|
||||
if (durationReader.getDurationUs() != C.TIME_UNSET) {
|
||||
psBinarySearchSeeker =
|
||||
new PsBinarySearchSeeker(
|
||||
durationReader.getScrTimestampAdjuster(),
|
||||
durationReader.getDurationUs(),
|
||||
inputLength);
|
||||
output.seekMap(psBinarySearchSeeker.getSeekMap());
|
||||
} else {
|
||||
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses PES packet data and extracts samples.
|
||||
|
BIN
library/core/src/test/assets/ts/elephants_dream.mpg
Normal file
BIN
library/core/src/test/assets/ts/elephants_dream.mpg
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
seekMap:
|
||||
isSeekable = false
|
||||
isSeekable = true
|
||||
duration = 766
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 2
|
||||
|
59
library/core/src/test/assets/ts/sample.ps.1.dump
Normal file
59
library/core/src/test/assets/ts/sample.ps.1.dump
Normal file
@ -0,0 +1,59 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 766
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 2
|
||||
track 192:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 192
|
||||
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 = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
track 224:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 224
|
||||
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 743CC6F8
|
||||
total output bytes = 33949
|
||||
sample count = 1
|
||||
sample 0:
|
||||
time = 80000
|
||||
flags = 0
|
||||
data = length 17831, hash 5C5A57F5
|
||||
tracksEnded = true
|
55
library/core/src/test/assets/ts/sample.ps.2.dump
Normal file
55
library/core/src/test/assets/ts/sample.ps.2.dump
Normal file
@ -0,0 +1,55 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 766
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 2
|
||||
track 192:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 192
|
||||
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 = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
track 224:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 224
|
||||
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 743CC6F8
|
||||
total output bytes = 19791
|
||||
sample count = 0
|
||||
tracksEnded = true
|
55
library/core/src/test/assets/ts/sample.ps.3.dump
Normal file
55
library/core/src/test/assets/ts/sample.ps.3.dump
Normal file
@ -0,0 +1,55 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 766
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 2
|
||||
track 192:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 192
|
||||
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 = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 0
|
||||
sample count = 0
|
||||
track 224:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 224
|
||||
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 743CC6F8
|
||||
total output bytes = 1585
|
||||
sample count = 0
|
||||
tracksEnded = true
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
/** Unit test for {@link PsDurationReader}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class PsDurationReaderTest {
|
||||
|
||||
private PsDurationReader tsDurationReader;
|
||||
private PositionHolder seekPositionHolder;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
tsDurationReader = new PsDurationReader();
|
||||
seekPositionHolder = new PositionHolder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsDurationReadPending_returnFalseByDefault() {
|
||||
assertThat(tsDurationReader.isDurationReadFinished()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException {
|
||||
FakeExtractorInput input =
|
||||
new FakeExtractorInput.Builder()
|
||||
.setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ps"))
|
||||
.build();
|
||||
|
||||
int result = Extractor.RESULT_CONTINUE;
|
||||
while (!tsDurationReader.isDurationReadFinished()) {
|
||||
result = tsDurationReader.readDuration(input, seekPositionHolder);
|
||||
if (result == Extractor.RESULT_SEEK) {
|
||||
input.setPosition((int) seekPositionHolder.position);
|
||||
}
|
||||
}
|
||||
assertThat(result).isNotEqualTo(Extractor.RESULT_END_OF_INPUT);
|
||||
assertThat(tsDurationReader.getDurationUs()).isEqualTo(766);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadDuration_midStream_returnsCorrectDuration()
|
||||
throws IOException, InterruptedException {
|
||||
FakeExtractorInput input =
|
||||
new FakeExtractorInput.Builder()
|
||||
.setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ps"))
|
||||
.build();
|
||||
|
||||
input.setPosition(1234);
|
||||
int result = Extractor.RESULT_CONTINUE;
|
||||
while (!tsDurationReader.isDurationReadFinished()) {
|
||||
result = tsDurationReader.readDuration(input, seekPositionHolder);
|
||||
if (result == Extractor.RESULT_SEEK) {
|
||||
input.setPosition((int) seekPositionHolder.position);
|
||||
}
|
||||
}
|
||||
assertThat(result).isNotEqualTo(Extractor.RESULT_END_OF_INPUT);
|
||||
assertThat(tsDurationReader.getDurationUs()).isEqualTo(766);
|
||||
}
|
||||
}
|
@ -0,0 +1,367 @@
|
||||
/*
|
||||
* 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.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||
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.FakeExtractorInput;
|
||||
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.DataSpec;
|
||||
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 PsExtractor}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class PsExtractorSeekTest {
|
||||
|
||||
private static final String PS_FILE_PATH = "ts/elephants_dream.mpg";
|
||||
private static final int DURATION_US = 30436333;
|
||||
private static final int VIDEO_TRACK_ID = 224;
|
||||
private static final long DELTA_TIMESTAMP_THRESHOLD_US = 500_000L;
|
||||
private static final Random random = new Random(1234L);
|
||||
|
||||
private FakeExtractorOutput expectedOutput;
|
||||
private FakeTrackOutput expectedTrackOutput;
|
||||
|
||||
private DefaultDataSource dataSource;
|
||||
private PositionHolder positionHolder;
|
||||
private long totalInputLength;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException, InterruptedException {
|
||||
expectedOutput = new FakeExtractorOutput();
|
||||
positionHolder = new PositionHolder();
|
||||
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, PS_FILE_PATH);
|
||||
expectedTrackOutput = expectedOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
dataSource =
|
||||
new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent")
|
||||
.createDataSource();
|
||||
totalInputLength = readInputLength();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPsExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
|
||||
throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
SeekMap seekMap = extractSeekMapAndTracks(extractor, new FakeExtractorOutput());
|
||||
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
long targetSeekTimeUs = 987_000;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekToEoF() throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
// Assert that this seek will return a position at end of stream, without any frame.
|
||||
assertThat(extractedFrameIndex).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
||||
|
||||
long targetSeekTimeUs = 0;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
||||
|
||||
long targetSeekTimeUs = 1_234_000;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
long numSeek = 100;
|
||||
for (long i = 0; i < numSeek; i++) {
|
||||
long targetSeekTimeUs = random.nextInt(DURATION_US + 1);
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
readInputFileOnce(extractor, extractorOutput);
|
||||
SeekMap seekMap = extractorOutput.seekMap;
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);
|
||||
|
||||
long numSeek = 100;
|
||||
for (long i = 0; i < numSeek; i++) {
|
||||
long targetSeekTimeUs = random.nextInt(DURATION_US + 1);
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private long readInputLength() throws IOException {
|
||||
DataSpec dataSpec =
|
||||
new DataSpec(Uri.parse("asset:///" + PS_FILE_PATH), 0, C.LENGTH_UNSET, null);
|
||||
long totalInputLength = dataSource.open(dataSpec);
|
||||
Util.closeQuietly(dataSource);
|
||||
return totalInputLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the given seek time and keeps reading from input until we can extract at least one
|
||||
* frame from the seek position, or until end-of-input is reached.
|
||||
*
|
||||
* @return The index of the first extracted frame written to the given {@code trackOutput} after
|
||||
* the seek is completed, or -1 if the seek is completed without any extracted frame.
|
||||
*/
|
||||
private int seekToTimeUs(
|
||||
PsExtractor psExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput)
|
||||
throws IOException, InterruptedException {
|
||||
int numSampleBeforeSeek = trackOutput.getSampleCount();
|
||||
SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
|
||||
|
||||
long initialSeekLoadPosition = seekPoints.first.position;
|
||||
psExtractor.seek(initialSeekLoadPosition, seekTimeUs);
|
||||
|
||||
positionHolder.position = C.POSITION_UNSET;
|
||||
ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition);
|
||||
int extractorReadResult = Extractor.RESULT_CONTINUE;
|
||||
while (true) {
|
||||
try {
|
||||
// Keep reading until we can read at least one frame after seek
|
||||
while (extractorReadResult == Extractor.RESULT_CONTINUE
|
||||
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
|
||||
extractorReadResult = psExtractor.read(extractorInput, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
|
||||
if (extractorReadResult == Extractor.RESULT_SEEK) {
|
||||
extractorInput = getExtractorInputFromPosition(positionHolder.position);
|
||||
extractorReadResult = Extractor.RESULT_CONTINUE;
|
||||
} else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {
|
||||
return -1;
|
||||
} else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
|
||||
// First index after seek = num sample before seek.
|
||||
return numSampleBeforeSeek;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SeekMap extractSeekMapAndTracks(PsExtractor extractor, FakeExtractorOutput output)
|
||||
throws IOException, InterruptedException {
|
||||
ExtractorInput input = getExtractorInputFromPosition(0);
|
||||
extractor.init(output);
|
||||
int readResult = Extractor.RESULT_CONTINUE;
|
||||
while (true) {
|
||||
try {
|
||||
// Keep reading until we can get the seek map
|
||||
while (readResult == Extractor.RESULT_CONTINUE
|
||||
&& (output.seekMap == null || !output.tracksEnded)) {
|
||||
readResult = extractor.read(input, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
|
||||
if (readResult == Extractor.RESULT_SEEK) {
|
||||
input = getExtractorInputFromPosition(positionHolder.position);
|
||||
readResult = Extractor.RESULT_CONTINUE;
|
||||
} else if (readResult == Extractor.RESULT_END_OF_INPUT) {
|
||||
throw new IOException("EOF encountered without seekmap");
|
||||
}
|
||||
if (output.seekMap != null) {
|
||||
return output.seekMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readInputFileOnce(PsExtractor extractor, FakeExtractorOutput extractorOutput)
|
||||
throws IOException, InterruptedException {
|
||||
extractor.init(extractorOutput);
|
||||
int readResult = Extractor.RESULT_CONTINUE;
|
||||
ExtractorInput input = getExtractorInputFromPosition(0);
|
||||
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 = getExtractorInputFromPosition(positionHolder.position);
|
||||
readResult = Extractor.RESULT_CONTINUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
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(C.INDEX_UNSET);
|
||||
|
||||
long sampleTimeUs = expectedTrackOutput.getSampleTimeUs(expectedSampleIndex);
|
||||
if (sampleTimeUs != 0) {
|
||||
// Assert that the timestamp output for first sample after seek is near the seek point.
|
||||
// For Ps seeking, unfortunately we can't guarantee exact frame seeking, since PID timestamp
|
||||
// is not too reliable.
|
||||
assertThat(Math.abs(outputSampleTimeUs - seekTimeUs))
|
||||
.isLessThan(DELTA_TIMESTAMP_THRESHOLD_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(DELTA_TIMESTAMP_THRESHOLD_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 C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {
|
||||
DataSpec dataSpec =
|
||||
new DataSpec(
|
||||
Uri.parse("asset:///" + PS_FILE_PATH), position, C.LENGTH_UNSET, /* key= */ null);
|
||||
dataSource.open(dataSpec);
|
||||
return new DefaultExtractorInput(dataSource, position, totalInputLength);
|
||||
}
|
||||
|
||||
private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)
|
||||
throws IOException, InterruptedException {
|
||||
byte[] data = TestUtil.getByteArray(context, fileName);
|
||||
|
||||
PsExtractor extractor = new PsExtractor();
|
||||
extractor.init(expectedOutput);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||
|
||||
int readResult = Extractor.RESULT_CONTINUE;
|
||||
while (readResult != Extractor.RESULT_END_OF_INPUT) {
|
||||
readResult = extractor.read(input, positionHolder);
|
||||
if (readResult == Extractor.RESULT_SEEK) {
|
||||
input.setPosition((int) positionHolder.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user