Skip to content

Commit

Permalink
Add supports for reading duration for a PS stream.
Browse files Browse the repository at this point in the history
Add supports for reading duration for a PS stream by reading SCR values from
the header of packs at the start and at the end of the stream, calculating the
difference, and converting that into stream duration.

Github: #4476

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=203954752
  • Loading branch information
botaydotcom authored and ojw28 committed Jul 11, 2018
1 parent 68822c0 commit 39b8122
Show file tree
Hide file tree
Showing 5 changed files with 347 additions and 3 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### dev-v2 (not yet released) ###

* MPEG-PS: Support reading duration from MPEG-PS Streams
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
* MediaSession extension:
* Allow apps to set custom errors.
* Audio:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
* 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.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException;

/**
* A reader that can extract the approximate duration from a given MPEG program stream (PS).
*
* <p>This reader extracts the duration by reading system clock reference (SCR) values from the
* header of a pack at the start and at the end of the stream, calculating the difference, and
* converting that into stream duration. This reader also handles the case when a single SCR
* wraparound takes place within the stream, which can make SCR values at the beginning of the
* stream larger than SCR values at the end. This class can only be used once to read duration from
* a given stream, and the usage of the class is not thread-safe, so all calls should be made from
* the same thread.
*
* <p>Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header.
*/
/* package */ final class PsDurationReader {

private static final int DURATION_READ_BYTES = 20000;

private final TimestampAdjuster scrTimestampAdjuster;
private final ParsableByteArray packetBuffer;

private boolean isDurationRead;
private boolean isFirstScrValueRead;
private boolean isLastScrValueRead;

private long firstScrValue;
private long lastScrValue;
private long durationUs;

/* package */ PsDurationReader() {
scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
firstScrValue = C.TIME_UNSET;
lastScrValue = C.TIME_UNSET;
durationUs = C.TIME_UNSET;
packetBuffer = new ParsableByteArray(DURATION_READ_BYTES);
}

/** Returns true if a PS duration has been read. */
public boolean isDurationReadFinished() {
return isDurationRead;
}

/**
* Reads a PS duration from the input.
*
* <p>This reader reads the duration by reading SCR values from the header of a pack at the start
* and at the end of the stream, calculating the difference, and converting that into stream
* duration.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* to hold the position of the required seek.
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public @Extractor.ReadResult int readDuration(
ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException {
if (!isLastScrValueRead) {
return readLastScrValue(input, seekPositionHolder);
}
if (lastScrValue == C.TIME_UNSET) {
return finishReadDuration(input);
}
if (!isFirstScrValueRead) {
return readFirstScrValue(input, seekPositionHolder);
}
if (firstScrValue == C.TIME_UNSET) {
return finishReadDuration(input);
}

long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue);
long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue);
durationUs = maxScrPositionUs - minScrPositionUs;
return finishReadDuration(input);
}

/** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */
public long getDurationUs() {
return durationUs;
}

private int finishReadDuration(ExtractorInput input) {
isDurationRead = true;
input.resetPeekPosition();
return Extractor.RESULT_CONTINUE;
}

private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException {
if (input.getPosition() != 0) {
seekPositionHolder.position = 0;
return Extractor.RESULT_SEEK;
}

int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);

firstScrValue = readFirstScrValueFromBuffer(packetBuffer);
isFirstScrValueRead = true;
return Extractor.RESULT_CONTINUE;
}

private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) {
int searchStartPosition = packetBuffer.getPosition();
int searchEndPosition = packetBuffer.limit();
for (int searchPosition = searchStartPosition;
searchPosition < searchEndPosition - 3;
searchPosition++) {
int nextStartCode = peakIntAtPosition(packetBuffer.data, searchPosition);
if (nextStartCode == PsExtractor.PACK_START_CODE) {
long scrValue = readScrValueFromPack(packetBuffer, searchPosition + 4);
if (scrValue != C.TIME_UNSET) {
return scrValue;
}
}
}
return C.TIME_UNSET;
}

private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException {
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
long bufferStartStreamPosition = input.getLength() - bytesToRead;
if (input.getPosition() != bufferStartStreamPosition) {
seekPositionHolder.position = bufferStartStreamPosition;
return Extractor.RESULT_SEEK;
}

input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);

lastScrValue = readLastScrValueFromBuffer(packetBuffer);
isLastScrValueRead = true;
return Extractor.RESULT_CONTINUE;
}

private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) {
int searchStartPosition = packetBuffer.getPosition();
int searchEndPosition = packetBuffer.limit();
for (int searchPosition = searchEndPosition - 4;
searchPosition >= searchStartPosition;
searchPosition--) {
int nextStartCode = peakIntAtPosition(packetBuffer.data, searchPosition);
if (nextStartCode == PsExtractor.PACK_START_CODE) {
long scrValue = readScrValueFromPack(packetBuffer, searchPosition + 4);
if (scrValue != C.TIME_UNSET) {
return scrValue;
}
}
}
return C.TIME_UNSET;
}

private int peakIntAtPosition(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) {
// Verify the 01xxx1xx marker on the 0th byte
if ((scrBytes[0] & 0xC4) != 0x44) {
return false;
}
// 1st byte belongs to scr field.
// Verify the xxxxx1xx marker on the 2nd byte
if ((scrBytes[2] & 0x04) != 0x04) {
return false;
}
// 3rd byte belongs to scr field.
// Verify the xxxxx1xx marker on the 4rd byte
if ((scrBytes[4] & 0x04) != 0x04) {
return false;
}
// Verify the xxxxxxx1 marker on the 5th byte
if ((scrBytes[5] & 0x01) != 0x01) {
return false;
}
// 6th and 7th bytes belongs to program_max_rate field.
// Verify the xxxxxx11 marker on the 8th byte
return (scrBytes[8] & 0x03) == 0x03;
}

/**
* Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring
* the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in
* pack_header.
*
* <p>We ignore SCR Ext, because it's too small to have any significance.
*/
private static long readScrValueFromPackHeader(byte[] scrBytes) {
return ((scrBytes[0] & 0b00111000L) >> 3) << 30
| (scrBytes[0] & 0b00000011L) << 28
| (scrBytes[1] & 0xFFL) << 20
| ((scrBytes[2] & 0b11111000L) >> 3) << 15
| (scrBytes[2] & 0b00000011L) << 13
| (scrBytes[3] & 0xFFL) << 5
| (scrBytes[4] & 0b11111000L) >> 3;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public Extractor[] createExtractors() {

};

private static final int PACK_START_CODE = 0x000001BA;
/* 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;
Expand All @@ -68,13 +68,16 @@ public Extractor[] createExtractors() {
private final TimestampAdjuster timestampAdjuster;
private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid
private final ParsableByteArray psPacketBuffer;
private final PsDurationReader durationReader;

private boolean foundAllTracks;
private boolean foundAudioTrack;
private boolean foundVideoTrack;
private long lastTrackPosition;

// Accessed only by the loading thread.
private ExtractorOutput output;
private boolean hasOutputSeekMap;

public PsExtractor() {
this(new TimestampAdjuster(0));
Expand All @@ -84,6 +87,7 @@ public PsExtractor(TimestampAdjuster timestampAdjuster) {
this.timestampAdjuster = timestampAdjuster;
psPacketBuffer = new ParsableByteArray(4096);
psPayloadReaders = new SparseArray<>();
durationReader = new PsDurationReader();
}

// Extractor implementation.
Expand Down Expand Up @@ -130,7 +134,6 @@ public boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept
@Override
public void init(ExtractorOutput output) {
this.output = output;
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
}

@Override
Expand All @@ -149,6 +152,13 @@ public void release() {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {

boolean canReadDuration = input.getLength() != C.LENGTH_UNSET;
if (canReadDuration && !durationReader.isDurationReadFinished()) {
return durationReader.readDuration(input, seekPosition);
}
maybeOutputSeekMap();

// First peek and check what type of start code is next.
if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
return RESULT_END_OF_INPUT;
Expand Down Expand Up @@ -250,6 +260,13 @@ public int read(ExtractorInput input, PositionHolder seekPosition)

// Internals.

private void maybeOutputSeekMap() {
if (!hasOutputSeekMap) {
hasOutputSeekMap = true;
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
}
}

/**
* Parses PES packet data and extracts samples.
*/
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/test/assets/ts/sample.ps.0.dump
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
seekMap:
isSeekable = false
duration = UNSET TIME
duration = 766
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2
track 192:
Expand Down
Loading

0 comments on commit 39b8122

Please sign in to comment.