blob: 3616a0c354bd40a60bd4339c543f7c79b276daeb [file] [log] [blame]
/*
* 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 java.lang.Math.min;
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 com.google.android.exoplayer2.util.Util;
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 = 20_000;
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 peek 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 to 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();
}
@Override
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
throws IOException {
long inputPosition = input.getPosition();
int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
}
@Override
public void onSeekFinished() {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
}
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.getData(), 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.getData(), 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.getData(), 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(
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);
}
}