mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
68822c0c41
commit
39b812298d
5 changed files with 347 additions and 3 deletions
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
### dev-v2 (not yet released) ###
|
### dev-v2 (not yet released) ###
|
||||||
|
|
||||||
|
* MPEG-PS: Support reading duration from MPEG-PS Streams
|
||||||
|
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
|
||||||
* MediaSession extension:
|
* MediaSession extension:
|
||||||
* Allow apps to set custom errors.
|
* Allow apps to set custom errors.
|
||||||
* Audio:
|
* Audio:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 SYSTEM_HEADER_START_CODE = 0x000001BB;
|
||||||
private static final int PACKET_START_CODE_PREFIX = 0x000001;
|
private static final int PACKET_START_CODE_PREFIX = 0x000001;
|
||||||
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
|
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 TimestampAdjuster timestampAdjuster;
|
||||||
private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid
|
private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid
|
||||||
private final ParsableByteArray psPacketBuffer;
|
private final ParsableByteArray psPacketBuffer;
|
||||||
|
private final PsDurationReader durationReader;
|
||||||
|
|
||||||
private boolean foundAllTracks;
|
private boolean foundAllTracks;
|
||||||
private boolean foundAudioTrack;
|
private boolean foundAudioTrack;
|
||||||
private boolean foundVideoTrack;
|
private boolean foundVideoTrack;
|
||||||
|
|
@ -75,6 +77,7 @@ public final class PsExtractor implements Extractor {
|
||||||
|
|
||||||
// Accessed only by the loading thread.
|
// Accessed only by the loading thread.
|
||||||
private ExtractorOutput output;
|
private ExtractorOutput output;
|
||||||
|
private boolean hasOutputSeekMap;
|
||||||
|
|
||||||
public PsExtractor() {
|
public PsExtractor() {
|
||||||
this(new TimestampAdjuster(0));
|
this(new TimestampAdjuster(0));
|
||||||
|
|
@ -84,6 +87,7 @@ public final class PsExtractor implements Extractor {
|
||||||
this.timestampAdjuster = timestampAdjuster;
|
this.timestampAdjuster = timestampAdjuster;
|
||||||
psPacketBuffer = new ParsableByteArray(4096);
|
psPacketBuffer = new ParsableByteArray(4096);
|
||||||
psPayloadReaders = new SparseArray<>();
|
psPayloadReaders = new SparseArray<>();
|
||||||
|
durationReader = new PsDurationReader();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extractor implementation.
|
// Extractor implementation.
|
||||||
|
|
@ -130,7 +134,6 @@ public final class PsExtractor implements Extractor {
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
this.output = output;
|
this.output = output;
|
||||||
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -149,6 +152,13 @@ public final class PsExtractor implements Extractor {
|
||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws IOException, InterruptedException {
|
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.
|
// First peek and check what type of start code is next.
|
||||||
if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
|
if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
|
|
@ -250,6 +260,13 @@ public final class PsExtractor implements Extractor {
|
||||||
|
|
||||||
// Internals.
|
// Internals.
|
||||||
|
|
||||||
|
private void maybeOutputSeekMap() {
|
||||||
|
if (!hasOutputSeekMap) {
|
||||||
|
hasOutputSeekMap = true;
|
||||||
|
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses PES packet data and extracts samples.
|
* Parses PES packet data and extracts samples.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
seekMap:
|
seekMap:
|
||||||
isSeekable = false
|
isSeekable = false
|
||||||
duration = UNSET TIME
|
duration = 766
|
||||||
getPosition(0) = [[timeUs=0, position=0]]
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
numberOfTracks = 2
|
numberOfTracks = 2
|
||||||
track 192:
|
track 192:
|
||||||
|
|
|
||||||
79
library/core/src/test/assets/ts/sample.ps.unklen.dump
Normal file
79
library/core/src/test/assets/ts/sample.ps.unklen.dump
Normal 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
|
||||||
Loading…
Reference in a new issue