Skip to content

Commit

Permalink
Supports seeking for MPEG PS Streams.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
botaydotcom authored and ojw28 committed Aug 1, 2018
1 parent 377314a commit f08ad55
Show file tree
Hide file tree
Showing 13 changed files with 917 additions and 59 deletions.
2 changes: 1 addition & 1 deletion RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,7 +74,6 @@ public TimestampSearchResult searchForTimestamp(
throws IOException, InterruptedException {
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
long searchPosition = input.getPosition();
int searchRangeBytes = getTimestampSearchBytesRange();
decoderJni.reset(searchPosition);
try {
decoderJni.decodeSampleWithBacktrackPosition(
Expand Down Expand Up @@ -107,13 +105,6 @@ public TimestampSearchResult searchForTimestamp(
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;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,6 @@ protected interface TimestampSeeker {
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();
}

/**
Expand All @@ -98,6 +90,18 @@ public OutputFrameHolder(ByteBuffer outputByteBuffer) {
}
}

/**
* 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}.
Expand Down Expand Up @@ -566,16 +570,4 @@ public long timeUsToTargetTime(long timeUs) {
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public boolean isDurationReadFinished() {
return isDurationRead;
}

public TimestampAdjuster getScrTimestampAdjuster() {
return scrTimestampAdjuster;
}

/**
* Reads a PS duration from the input.
*
Expand Down Expand Up @@ -105,6 +109,25 @@ public long getDurationUs() {
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();
Expand Down Expand Up @@ -135,9 +158,10 @@ private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) {
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;
}
Expand Down Expand Up @@ -171,9 +195,10 @@ private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) {
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;
}
Expand All @@ -182,28 +207,14 @@ private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) {
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;
Expand Down
Loading

0 comments on commit f08ad55

Please sign in to comment.