Add supports for reading duration for a PS stream.

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
This commit is contained in:
hoangtc 2018-07-10 08:57:07 -07:00 committed by Oliver Woodman
parent 68822c0c41
commit 39b812298d
5 changed files with 347 additions and 3 deletions

View file

@ -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:

View file

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

View file

@ -47,7 +47,7 @@ public final class PsExtractor implements Extractor {
};
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;
@ -68,6 +68,8 @@ public final class PsExtractor implements Extractor {
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;
@ -75,6 +77,7 @@ public final class PsExtractor implements Extractor {
// Accessed only by the loading thread.
private ExtractorOutput output;
private boolean hasOutputSeekMap;
public PsExtractor() {
this(new TimestampAdjuster(0));
@ -84,6 +87,7 @@ public final class PsExtractor implements Extractor {
this.timestampAdjuster = timestampAdjuster;
psPacketBuffer = new ParsableByteArray(4096);
psPayloadReaders = new SparseArray<>();
durationReader = new PsDurationReader();
}
// Extractor implementation.
@ -130,7 +134,6 @@ public final class PsExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
this.output = output;
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
}
@Override
@ -149,6 +152,13 @@ public final class PsExtractor implements Extractor {
@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;
@ -250,6 +260,13 @@ public final class PsExtractor implements Extractor {
// Internals.
private void maybeOutputSeekMap() {
if (!hasOutputSeekMap) {
hasOutputSeekMap = true;
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
}
}
/**
* Parses PES packet data and extracts samples.
*/

View file

@ -1,6 +1,6 @@
seekMap:
isSeekable = false
duration = UNSET TIME
duration = 766
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2
track 192:

View file

@ -0,0 +1,79 @@
seekMap:
isSeekable = false
duration = UNSET TIME
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 = 1671
sample count = 4
sample 0:
time = 29088
flags = 1
data = length 417, hash 5C710F78
sample 1:
time = 55210
flags = 1
data = length 418, hash 79CF71F8
sample 2:
time = 81332
flags = 1
data = length 418, hash 79CF71F8
sample 3:
time = 107454
flags = 1
data = length 418, hash 79CF71F8
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 = 44056
sample count = 2
sample 0:
time = 40000
flags = 1
data = length 20646, hash 576390B
sample 1:
time = 80000
flags = 0
data = length 17831, hash 5C5A57F5
tracksEnded = true