mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
HLS improvements + steps towards ABR.
This commit is contained in:
parent
9790430a62
commit
aeb17e6a88
4 changed files with 182 additions and 113 deletions
|
|
@ -45,7 +45,6 @@ public class HlsChunkSource {
|
||||||
private final HlsMasterPlaylist masterPlaylist;
|
private final HlsMasterPlaylist masterPlaylist;
|
||||||
private final HlsMediaPlaylistParser mediaPlaylistParser;
|
private final HlsMediaPlaylistParser mediaPlaylistParser;
|
||||||
|
|
||||||
private long liveStartTimeUs;
|
|
||||||
/* package */ HlsMediaPlaylist mediaPlaylist;
|
/* package */ HlsMediaPlaylist mediaPlaylist;
|
||||||
/* package */ boolean mediaPlaylistWasLive;
|
/* package */ boolean mediaPlaylistWasLive;
|
||||||
/* package */ long lastMediaPlaylistLoadTimeMs;
|
/* package */ long lastMediaPlaylistLoadTimeMs;
|
||||||
|
|
@ -168,25 +167,22 @@ public class HlsChunkSource {
|
||||||
|
|
||||||
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
|
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
|
||||||
|
|
||||||
long startTimeUs = segment.startTimeUs;
|
long startTimeUs;
|
||||||
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
|
|
||||||
int nextChunkMediaSequence = chunkMediaSequence + 1;
|
int nextChunkMediaSequence = chunkMediaSequence + 1;
|
||||||
|
|
||||||
if (mediaPlaylistWasLive) {
|
if (mediaPlaylistWasLive) {
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
liveStartTimeUs = startTimeUs;
|
|
||||||
startTimeUs = 0;
|
startTimeUs = 0;
|
||||||
endTimeUs -= liveStartTimeUs;
|
|
||||||
} else {
|
} else {
|
||||||
startTimeUs -= liveStartTimeUs;
|
startTimeUs = queue.get(queue.size() - 1).endTimeUs;
|
||||||
endTimeUs -= liveStartTimeUs;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not live.
|
// Not live.
|
||||||
|
startTimeUs = segment.startTimeUs;
|
||||||
if (chunkIndex == mediaPlaylist.segments.size() - 1) {
|
if (chunkIndex == mediaPlaylist.segments.size() - 1) {
|
||||||
nextChunkMediaSequence = -1;
|
nextChunkMediaSequence = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
|
||||||
|
|
||||||
DataSource dataSource;
|
DataSource dataSource;
|
||||||
if (encryptedDataSource != null) {
|
if (encryptedDataSource != null) {
|
||||||
|
|
@ -194,9 +190,8 @@ public class HlsChunkSource {
|
||||||
} else {
|
} else {
|
||||||
dataSource = upstreamDataSource;
|
dataSource = upstreamDataSource;
|
||||||
}
|
}
|
||||||
|
out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs,
|
||||||
out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs,
|
nextChunkMediaSequence, segment.discontinuity, false);
|
||||||
nextChunkMediaSequence, segment.discontinuity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldRerequestMediaPlaylist() {
|
private boolean shouldRerequestMediaPlaylist() {
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000;
|
private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000;
|
||||||
private static final int NO_RESET_PENDING = -1;
|
private static final int NO_RESET_PENDING = -1;
|
||||||
|
|
||||||
private final TsExtractor extractor;
|
private final TsExtractor.SamplePool samplePool;
|
||||||
private final LoadControl loadControl;
|
private final LoadControl loadControl;
|
||||||
private final HlsChunkSource chunkSource;
|
private final HlsChunkSource chunkSource;
|
||||||
private final HlsChunkOperationHolder currentLoadableHolder;
|
private final HlsChunkOperationHolder currentLoadableHolder;
|
||||||
|
private final LinkedList<TsExtractor> extractors;
|
||||||
private final LinkedList<TsChunk> mediaChunks;
|
private final LinkedList<TsChunk> mediaChunks;
|
||||||
private final List<TsChunk> readOnlyHlsChunks;
|
private final List<TsChunk> readOnlyHlsChunks;
|
||||||
private final int bufferSizeContribution;
|
private final int bufferSizeContribution;
|
||||||
|
|
@ -84,7 +85,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
this.bufferSizeContribution = bufferSizeContribution;
|
this.bufferSizeContribution = bufferSizeContribution;
|
||||||
this.frameAccurateSeeking = frameAccurateSeeking;
|
this.frameAccurateSeeking = frameAccurateSeeking;
|
||||||
this.remainingReleaseCount = downstreamRendererCount;
|
this.remainingReleaseCount = downstreamRendererCount;
|
||||||
extractor = new TsExtractor();
|
samplePool = new TsExtractor.SamplePool();
|
||||||
|
extractors = new LinkedList<TsExtractor>();
|
||||||
currentLoadableHolder = new HlsChunkOperationHolder();
|
currentLoadableHolder = new HlsChunkOperationHolder();
|
||||||
mediaChunks = new LinkedList<TsChunk>();
|
mediaChunks = new LinkedList<TsChunk>();
|
||||||
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
|
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
|
||||||
|
|
@ -100,6 +102,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
loadControl.register(this, bufferSizeContribution);
|
loadControl.register(this, bufferSizeContribution);
|
||||||
}
|
}
|
||||||
continueBufferingInternal();
|
continueBufferingInternal();
|
||||||
|
if (extractors.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TsExtractor extractor = extractors.get(0);
|
||||||
if (extractor.isPrepared()) {
|
if (extractor.isPrepared()) {
|
||||||
trackCount = extractor.getTrackCount();
|
trackCount = extractor.getTrackCount();
|
||||||
trackEnabledStates = new boolean[trackCount];
|
trackEnabledStates = new boolean[trackCount];
|
||||||
|
|
@ -171,39 +177,38 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
TsChunk mediaChunk = mediaChunks.getFirst();
|
TsChunk mediaChunk = mediaChunks.getFirst();
|
||||||
|
int currentVariant = mediaChunk.variantIndex;
|
||||||
|
|
||||||
|
TsExtractor extractor;
|
||||||
|
if (extractors.isEmpty()) {
|
||||||
|
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
|
||||||
|
extractors.addLast(extractor);
|
||||||
|
if (mediaChunk.discardFromFirstKeyframes) {
|
||||||
|
extractor.discardFromNextKeyframes();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extractor = extractors.getLast();
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) {
|
if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) {
|
||||||
discardDownstreamHlsChunk();
|
discardDownstreamHlsChunk();
|
||||||
mediaChunk = mediaChunks.getFirst();
|
mediaChunk = mediaChunks.getFirst();
|
||||||
}
|
if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) {
|
||||||
|
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
|
||||||
boolean haveSufficientSamples = false;
|
extractors.addLast(extractor);
|
||||||
if (mediaChunk.hasPendingDiscontinuity()) {
|
}
|
||||||
if (extractor.hasSamples()) {
|
if (mediaChunk.discardFromFirstKeyframes) {
|
||||||
// There are samples from before the discontinuity yet to be read from the extractor, so
|
extractor.discardFromNextKeyframes();
|
||||||
// we don't want to reset the extractor yet.
|
|
||||||
haveSufficientSamples = true;
|
|
||||||
} else {
|
|
||||||
extractor.reset(mediaChunk.startTimeUs);
|
|
||||||
mediaChunk.clearPendingDiscontinuity();
|
|
||||||
if (pendingDiscontinuities == null) {
|
|
||||||
// We're not prepared yet.
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
|
||||||
pendingDiscontinuities[i] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaChunk.hasPendingDiscontinuity()) {
|
// Allow the extractor to consume from the current chunk.
|
||||||
// Allow the extractor to consume from the current chunk.
|
NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream();
|
||||||
NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream();
|
boolean haveSufficientSamples = extractor.consumeUntil(inputStream,
|
||||||
haveSufficientSamples = extractor.consumeUntil(inputStream,
|
downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US);
|
||||||
downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US);
|
if (!haveSufficientSamples) {
|
||||||
// If we can't read any more, then we always say we have sufficient samples.
|
// If we can't read any more, then we always say we have sufficient samples.
|
||||||
if (!haveSufficientSamples) {
|
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
|
||||||
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!haveSufficientSamples && currentLoadableException != null) {
|
if (!haveSufficientSamples && currentLoadableException != null) {
|
||||||
|
|
@ -223,7 +228,28 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
return DISCONTINUITY_READ;
|
return DISCONTINUITY_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) {
|
if (onlyReadDiscontinuity || isPendingReset()) {
|
||||||
|
return NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractors.isEmpty()) {
|
||||||
|
return NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
TsExtractor extractor = extractors.getFirst();
|
||||||
|
while (extractors.size() > 1 && !extractor.hasSamples()) {
|
||||||
|
// We're finished reading from the extractor for all tracks, and so can discard it.
|
||||||
|
extractors.removeFirst().clear();
|
||||||
|
extractor = extractors.getFirst();
|
||||||
|
}
|
||||||
|
int extractorIndex = 0;
|
||||||
|
while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) {
|
||||||
|
// We're finished reading from the extractor for this particular track, so advance to the
|
||||||
|
// next one for the current read.
|
||||||
|
extractor = extractors.get(++extractorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extractor.isPrepared()) {
|
||||||
return NOTHING_READ;
|
return NOTHING_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,9 +291,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
if (mediaChunk == null) {
|
if (mediaChunk == null) {
|
||||||
restartFrom(positionUs);
|
restartFrom(positionUs);
|
||||||
} else {
|
} else {
|
||||||
|
discardExtractors();
|
||||||
discardDownstreamHlsChunks(mediaChunk);
|
discardDownstreamHlsChunks(mediaChunk);
|
||||||
mediaChunk.reset();
|
mediaChunk.resetReadPosition();
|
||||||
extractor.reset(mediaChunk.startTimeUs);
|
|
||||||
updateLoadControl();
|
updateLoadControl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -494,13 +520,20 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
TsChunk mediaChunk = (TsChunk) currentLoadable;
|
TsChunk mediaChunk = (TsChunk) currentLoadable;
|
||||||
mediaChunks.add(mediaChunk);
|
mediaChunks.add(mediaChunk);
|
||||||
if (isPendingReset()) {
|
if (isPendingReset()) {
|
||||||
extractor.reset(mediaChunk.startTimeUs);
|
discardExtractors();
|
||||||
pendingResetPositionUs = NO_RESET_PENDING;
|
pendingResetPositionUs = NO_RESET_PENDING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loader.startLoading(currentLoadable, this);
|
loader.startLoading(currentLoadable, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void discardExtractors() {
|
||||||
|
for (int i = 0; i < extractors.size(); i++) {
|
||||||
|
extractors.get(i).clear();
|
||||||
|
}
|
||||||
|
extractors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
|
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
|
||||||
* itself discarded. Null can be passed to discard all media chunks.
|
* itself discarded. Null can be passed to discard all media chunks.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
*/
|
*/
|
||||||
public final class TsChunk extends HlsChunk {
|
public final class TsChunk extends HlsChunk {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the variant in the master playlist.
|
||||||
|
*/
|
||||||
|
public final int variantIndex;
|
||||||
/**
|
/**
|
||||||
* The start time of the media contained by the chunk.
|
* The start time of the media contained by the chunk.
|
||||||
*/
|
*/
|
||||||
|
|
@ -38,44 +42,38 @@ public final class TsChunk extends HlsChunk {
|
||||||
/**
|
/**
|
||||||
* The encoding discontinuity indicator.
|
* The encoding discontinuity indicator.
|
||||||
*/
|
*/
|
||||||
private final boolean discontinuity;
|
public final boolean discontinuity;
|
||||||
|
/**
|
||||||
private boolean pendingDiscontinuity;
|
* For each track, whether samples from the first keyframe (inclusive) should be discarded.
|
||||||
|
*/
|
||||||
|
public final boolean discardFromFirstKeyframes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param dataSource A {@link DataSource} for loading the data.
|
* @param dataSource A {@link DataSource} for loading the data.
|
||||||
* @param dataSpec Defines the data to be loaded.
|
* @param dataSpec Defines the data to be loaded.
|
||||||
* @param trigger The reason for this chunk being selected.
|
* @param trigger The reason for this chunk being selected.
|
||||||
|
* @param variantIndex The index of the variant in the master playlist.
|
||||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||||
* @param discontinuity The encoding discontinuity indicator.
|
* @param discontinuity The encoding discontinuity indicator.
|
||||||
|
* @param discardFromFirstKeyframes For each contained media stream, whether samples from the
|
||||||
|
* first keyframe (inclusive) should be discarded.
|
||||||
*/
|
*/
|
||||||
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs,
|
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex,
|
||||||
long endTimeUs, int nextChunkIndex, boolean discontinuity) {
|
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity,
|
||||||
|
boolean discardFromFirstKeyframes) {
|
||||||
super(dataSource, dataSpec, trigger);
|
super(dataSource, dataSpec, trigger);
|
||||||
|
this.variantIndex = variantIndex;
|
||||||
this.startTimeUs = startTimeUs;
|
this.startTimeUs = startTimeUs;
|
||||||
this.endTimeUs = endTimeUs;
|
this.endTimeUs = endTimeUs;
|
||||||
this.nextChunkIndex = nextChunkIndex;
|
this.nextChunkIndex = nextChunkIndex;
|
||||||
this.discontinuity = discontinuity;
|
this.discontinuity = discontinuity;
|
||||||
this.pendingDiscontinuity = discontinuity;
|
this.discardFromFirstKeyframes = discardFromFirstKeyframes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLastChunk() {
|
public boolean isLastChunk() {
|
||||||
return nextChunkIndex == -1;
|
return nextChunkIndex == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
|
||||||
resetReadPosition();
|
|
||||||
pendingDiscontinuity = discontinuity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasPendingDiscontinuity() {
|
|
||||||
return pendingDiscontinuity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearPendingDiscontinuity() {
|
|
||||||
pendingDiscontinuity = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,13 @@ import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaExtractor;
|
import android.media.MediaExtractor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
@ -47,26 +49,27 @@ public final class TsExtractor {
|
||||||
private static final int TS_STREAM_TYPE_H264 = 0x1B;
|
private static final int TS_STREAM_TYPE_H264 = 0x1B;
|
||||||
private static final int TS_STREAM_TYPE_ID3 = 0x15;
|
private static final int TS_STREAM_TYPE_ID3 = 0x15;
|
||||||
|
|
||||||
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
|
|
||||||
|
|
||||||
private final BitsArray tsPacketBuffer;
|
private final BitsArray tsPacketBuffer;
|
||||||
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
|
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
|
||||||
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||||
private final Queue<Sample> samplesPool;
|
private final SamplePool samplePool;
|
||||||
|
|
||||||
private boolean prepared;
|
private boolean prepared;
|
||||||
|
|
||||||
/* package */ boolean pendingTimestampOffsetUpdate;
|
/* package */ boolean pendingFirstSampleTimestampAdjustment;
|
||||||
/* package */ long pendingTimestampOffsetUs;
|
/* package */ long firstSampleTimestamp;
|
||||||
/* package */ long sampleTimestampOffsetUs;
|
/* package */ long sampleTimestampOffsetUs;
|
||||||
/* package */ long largestParsedTimestampUs;
|
/* package */ long largestParsedTimestampUs;
|
||||||
|
/* package */ boolean discardFromNextKeyframes;
|
||||||
|
|
||||||
public TsExtractor() {
|
public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) {
|
||||||
|
this.firstSampleTimestamp = firstSampleTimestamp;
|
||||||
|
this.samplePool = samplePool;
|
||||||
|
pendingFirstSampleTimestampAdjustment = true;
|
||||||
tsPacketBuffer = new BitsArray();
|
tsPacketBuffer = new BitsArray();
|
||||||
pesPayloadReaders = new SparseArray<PesPayloadReader>();
|
pesPayloadReaders = new SparseArray<PesPayloadReader>();
|
||||||
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
||||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||||
samplesPool = new LinkedList<Sample>();
|
|
||||||
largestParsedTimestampUs = Long.MIN_VALUE;
|
largestParsedTimestampUs = Long.MIN_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,22 +108,19 @@ public final class TsExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the extractor's internal state.
|
* Flushes any pending or incomplete samples, returning them to the sample pool.
|
||||||
*/
|
*/
|
||||||
public void reset(long nextSampleTimestampUs) {
|
public void clear() {
|
||||||
prepared = false;
|
|
||||||
tsPacketBuffer.reset();
|
|
||||||
tsPayloadReaders.clear();
|
|
||||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
|
||||||
// Clear each reader before discarding it, so as to recycle any queued Sample objects.
|
|
||||||
for (int i = 0; i < pesPayloadReaders.size(); i++) {
|
for (int i = 0; i < pesPayloadReaders.size(); i++) {
|
||||||
pesPayloadReaders.valueAt(i).clear();
|
pesPayloadReaders.valueAt(i).clear();
|
||||||
}
|
}
|
||||||
pesPayloadReaders.clear();
|
}
|
||||||
// Configure for subsequent read operations.
|
|
||||||
pendingTimestampOffsetUpdate = true;
|
/**
|
||||||
pendingTimestampOffsetUs = nextSampleTimestampUs;
|
* For each track, whether to discard samples from the next keyframe (inclusive).
|
||||||
largestParsedTimestampUs = Long.MIN_VALUE;
|
*/
|
||||||
|
public void discardFromNextKeyframes() {
|
||||||
|
discardFromNextKeyframes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -153,31 +153,43 @@ public final class TsExtractor {
|
||||||
*/
|
*/
|
||||||
public boolean getSample(int track, SampleHolder out) {
|
public boolean getSample(int track, SampleHolder out) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Queue<Sample> queue = pesPayloadReaders.valueAt(track).samplesQueue;
|
Queue<Sample> queue = pesPayloadReaders.valueAt(track).sampleQueue;
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Sample sample = queue.remove();
|
Sample sample = queue.remove();
|
||||||
convert(sample, out);
|
convert(sample, out);
|
||||||
recycleSample(sample);
|
samplePool.recycle(sample);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)}.
|
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for any
|
||||||
|
* track.
|
||||||
*
|
*
|
||||||
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}.
|
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
|
||||||
* False otherwise.
|
* for any track. False otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean hasSamples() {
|
public boolean hasSamples() {
|
||||||
for (int i = 0; i < pesPayloadReaders.size(); i++) {
|
for (int i = 0; i < pesPayloadReaders.size(); i++) {
|
||||||
if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) {
|
if (hasSamples(i)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
|
||||||
|
* specified track.
|
||||||
|
*
|
||||||
|
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
|
||||||
|
* for the specified track. False otherwise.
|
||||||
|
*/
|
||||||
|
public boolean hasSamples(int track) {
|
||||||
|
return !pesPayloadReaders.valueAt(track).sampleQueue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean checkPrepared() {
|
private boolean checkPrepared() {
|
||||||
int pesPayloadReaderCount = pesPayloadReaders.size();
|
int pesPayloadReaderCount = pesPayloadReaders.size();
|
||||||
if (pesPayloadReaderCount == 0) {
|
if (pesPayloadReaderCount == 0) {
|
||||||
|
|
@ -251,18 +263,6 @@ public final class TsExtractor {
|
||||||
out.timeUs = in.timeUs;
|
out.timeUs = in.timeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ Sample getSample() {
|
|
||||||
if (samplesPool.isEmpty()) {
|
|
||||||
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
|
|
||||||
}
|
|
||||||
return samplesPool.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ void recycleSample(Sample sample) {
|
|
||||||
sample.reset();
|
|
||||||
samplesPool.add(sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses payload data.
|
* Parses payload data.
|
||||||
*/
|
*/
|
||||||
|
|
@ -484,12 +484,14 @@ public final class TsExtractor {
|
||||||
*/
|
*/
|
||||||
private abstract class PesPayloadReader {
|
private abstract class PesPayloadReader {
|
||||||
|
|
||||||
public final Queue<Sample> samplesQueue;
|
public final Queue<Sample> sampleQueue;
|
||||||
|
|
||||||
private MediaFormat mediaFormat;
|
private MediaFormat mediaFormat;
|
||||||
|
private boolean foundFirstKeyframe;
|
||||||
|
private boolean foundLastKeyframe;
|
||||||
|
|
||||||
protected PesPayloadReader() {
|
protected PesPayloadReader() {
|
||||||
this.samplesQueue = new LinkedList<Sample>();
|
this.sampleQueue = new LinkedList<Sample>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasMediaFormat() {
|
public boolean hasMediaFormat() {
|
||||||
|
|
@ -507,8 +509,8 @@ public final class TsExtractor {
|
||||||
public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs);
|
public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs);
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
while (!samplesQueue.isEmpty()) {
|
while (!sampleQueue.isEmpty()) {
|
||||||
recycleSample(samplesQueue.remove());
|
samplePool.recycle(sampleQueue.remove());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,17 +522,31 @@ public final class TsExtractor {
|
||||||
* @param sampleTimeUs The sample time stamp.
|
* @param sampleTimeUs The sample time stamp.
|
||||||
*/
|
*/
|
||||||
protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) {
|
protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) {
|
||||||
Sample sample = getSample();
|
Sample sample = samplePool.get();
|
||||||
addToSample(sample, buffer, sampleSize);
|
addToSample(sample, buffer, sampleSize);
|
||||||
sample.flags = flags;
|
sample.flags = flags;
|
||||||
sample.timeUs = sampleTimeUs;
|
sample.timeUs = sampleTimeUs;
|
||||||
addSample(sample);
|
addSample(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
protected void addSample(Sample sample) {
|
protected void addSample(Sample sample) {
|
||||||
|
boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
|
||||||
|
if (isKeyframe) {
|
||||||
|
if (!foundFirstKeyframe) {
|
||||||
|
foundFirstKeyframe = true;
|
||||||
|
}
|
||||||
|
if (discardFromNextKeyframes) {
|
||||||
|
foundLastKeyframe = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
adjustTimestamp(sample);
|
adjustTimestamp(sample);
|
||||||
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
|
if (foundFirstKeyframe && !foundLastKeyframe) {
|
||||||
samplesQueue.add(sample);
|
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
|
||||||
|
sampleQueue.add(sample);
|
||||||
|
} else {
|
||||||
|
samplePool.recycle(sample);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addToSample(Sample sample, BitsArray buffer, int size) {
|
protected void addToSample(Sample sample, BitsArray buffer, int size) {
|
||||||
|
|
@ -542,9 +558,9 @@ public final class TsExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void adjustTimestamp(Sample sample) {
|
private void adjustTimestamp(Sample sample) {
|
||||||
if (pendingTimestampOffsetUpdate) {
|
if (pendingFirstSampleTimestampAdjustment) {
|
||||||
sampleTimestampOffsetUs = pendingTimestampOffsetUs - sample.timeUs;
|
sampleTimestampOffsetUs = firstSampleTimestamp - sample.timeUs;
|
||||||
pendingTimestampOffsetUpdate = false;
|
pendingFirstSampleTimestampAdjustment = false;
|
||||||
}
|
}
|
||||||
sample.timeUs += sampleTimestampOffsetUs;
|
sample.timeUs += sampleTimestampOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
@ -583,7 +599,7 @@ public final class TsExtractor {
|
||||||
if (currentSample != null) {
|
if (currentSample != null) {
|
||||||
addSample(currentSample);
|
addSample(currentSample);
|
||||||
}
|
}
|
||||||
currentSample = getSample();
|
currentSample = samplePool.get();
|
||||||
pesPayloadSize -= readOneH264Frame(pesBuffer, false);
|
pesPayloadSize -= readOneH264Frame(pesBuffer, false);
|
||||||
currentSample.timeUs = pesTimeUs;
|
currentSample.timeUs = pesTimeUs;
|
||||||
|
|
||||||
|
|
@ -615,7 +631,7 @@ public final class TsExtractor {
|
||||||
public void clear() {
|
public void clear() {
|
||||||
super.clear();
|
super.clear();
|
||||||
if (currentSample != null) {
|
if (currentSample != null) {
|
||||||
recycleSample(currentSample);
|
samplePool.recycle(currentSample);
|
||||||
currentSample = null;
|
currentSample = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -742,8 +758,35 @@ public final class TsExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplified version of SampleHolder for internal buffering.
|
* A pool from which the extractor can obtain sample objects for internal use.
|
||||||
*/
|
*/
|
||||||
|
public static class SamplePool {
|
||||||
|
|
||||||
|
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||||
|
|
||||||
|
private final ArrayList<Sample> samples;
|
||||||
|
|
||||||
|
public SamplePool() {
|
||||||
|
samples = new ArrayList<Sample>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ Sample get() {
|
||||||
|
if (samples.isEmpty()) {
|
||||||
|
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
|
}
|
||||||
|
return samples.remove(samples.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ void recycle(Sample sample) {
|
||||||
|
sample.reset();
|
||||||
|
samples.add(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified version of SampleHolder for internal buffering.
|
||||||
|
*/
|
||||||
private static class Sample {
|
private static class Sample {
|
||||||
|
|
||||||
public byte[] data;
|
public byte[] data;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue