Continue TsExtractor refactor.

- Remove TsExtractor's knowledge of Sample.
- Push handling of Sample objects into SampleQueue as much
  as possible. This is a precursor to replacing Sample objects
  with a different type of backing memory. Ideally, the
  individual readers shouldn't know how the sample data is
  stored. This is true after this CL, with the except of the
  TODO in H264Reader.
- Avoid double-scanning every H264 sample for NAL units, by
  moving the scan for SEI units from SeiReader into H264Reader.

Issue: #278
This commit is contained in:
Oliver Woodman 2015-02-12 17:24:23 +00:00
parent 61a86295fd
commit 066334dad7
7 changed files with 130 additions and 164 deletions

View file

@ -53,7 +53,6 @@ import java.util.Collections;
// Used when reading the samples.
private long timeUs;
private Sample currentSample;
public AdtsReader(SamplePool samplePool) {
super(samplePool);
@ -78,20 +77,17 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader();
currentSample = getSample(Sample.TYPE_AUDIO);
currentSample.timeUs = timeUs;
currentSample.isKeyframe = true;
startSample(Sample.TYPE_AUDIO, timeUs);
bytesRead = 0;
state = STATE_READING_SAMPLE;
}
break;
case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
addToSample(currentSample, data, bytesToRead);
appendSampleData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
addSample(currentSample);
currentSample = null;
commitSample(true);
timeUs += frameDurationUs;
bytesRead = 0;
state = STATE_FINDING_SYNC;
@ -106,15 +102,6 @@ import java.util.Collections;
// Do nothing.
}
@Override
public void release() {
super.release();
if (currentSample != null) {
recycle(currentSample);
currentSample = null;
}
}
/**
* Continues a read from the provided {@code source} into a given {@code target}. It's assumed
* that the data should be written into {@code target} starting from an offset of zero.

View file

@ -30,14 +30,13 @@ import java.util.List;
/* package */ class H264Reader extends PesPayloadReader {
private static final int NAL_UNIT_TYPE_IDR = 5;
private static final int NAL_UNIT_TYPE_SEI = 6;
private static final int NAL_UNIT_TYPE_SPS = 7;
private static final int NAL_UNIT_TYPE_PPS = 8;
private static final int NAL_UNIT_TYPE_AUD = 9;
private final SeiReader seiReader;
private Sample currentSample;
public H264Reader(SamplePool samplePool, SeiReader seiReader) {
super(samplePool);
this.seiReader = seiReader;
@ -47,14 +46,32 @@ import java.util.List;
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
while (data.bytesLeft() > 0) {
if (readToNextAudUnit(data, pesTimeUs)) {
currentSample.isKeyframe = currentSample.size
> Mp4Util.findNalUnit(currentSample.data, 0, currentSample.size, NAL_UNIT_TYPE_IDR);
if (!hasMediaFormat() && currentSample.isKeyframe) {
parseMediaFormat(currentSample);
// TODO: Allowing access to the Sample object here is messy. Fix this.
Sample pendingSample = getPendingSample();
byte[] pendingSampleData = pendingSample.data;
int pendingSampleSize = pendingSample.size;
// Scan the sample to find relevant NAL units.
int position = 0;
int idrNalUnitPosition = Integer.MAX_VALUE;
while (position < pendingSampleSize) {
position = Mp4Util.findNalUnit(pendingSampleData, position, pendingSampleSize);
if (position < pendingSampleSize) {
int type = Mp4Util.getNalUnitType(pendingSampleData, position);
if (type == NAL_UNIT_TYPE_IDR) {
idrNalUnitPosition = position;
} else if (type == NAL_UNIT_TYPE_SEI) {
seiReader.read(pendingSampleData, position, pendingSample.timeUs);
}
position += 4;
}
}
seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs);
addSample(currentSample);
currentSample = null;
boolean isKeyframe = pendingSampleSize > idrNalUnitPosition;
if (!hasMediaFormat() && isKeyframe) {
parseMediaFormat(pendingSampleData, pendingSampleSize);
}
commitSample(isKeyframe);
}
}
}
@ -64,15 +81,6 @@ import java.util.List;
// Do nothing.
}
@Override
public void release() {
super.release();
if (currentSample != null) {
recycle(currentSample);
currentSample = null;
}
}
/**
* Reads data up to (but not including) the start of the next AUD unit.
*
@ -89,16 +97,15 @@ import java.util.List;
int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD);
int bytesToNextAud = audOffset - pesOffset;
if (bytesToNextAud == 0) {
if (currentSample == null) {
currentSample = getSample(Sample.TYPE_VIDEO);
currentSample.timeUs = pesTimeUs;
addToSample(currentSample, data, 4);
if (!havePendingSample()) {
startSample(Sample.TYPE_VIDEO, pesTimeUs);
appendSampleData(data, 4);
return false;
} else {
return true;
}
} else if (currentSample != null) {
addToSample(currentSample, data, bytesToNextAud);
} else if (havePendingSample()) {
appendSampleData(data, bytesToNextAud);
return data.bytesLeft() > 0;
} else {
data.skip(bytesToNextAud);
@ -106,9 +113,7 @@ import java.util.List;
}
}
private void parseMediaFormat(Sample sample) {
byte[] sampleData = sample.data;
int sampleSize = sample.size;
private void parseMediaFormat(byte[] sampleData, int sampleSize) {
// Locate the SPS and PPS units.
int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS);
int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS);

View file

@ -25,8 +25,6 @@ import android.annotation.SuppressLint;
*/
/* package */ class Id3Reader extends PesPayloadReader {
private Sample currentSample;
public Id3Reader(SamplePool samplePool) {
super(samplePool);
setMediaFormat(MediaFormat.createId3Format());
@ -36,28 +34,16 @@ import android.annotation.SuppressLint;
@Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) {
currentSample = getSample(Sample.TYPE_MISC);
currentSample.timeUs = pesTimeUs;
currentSample.isKeyframe = true;
startSample(Sample.TYPE_MISC, pesTimeUs);
}
if (currentSample != null) {
addToSample(currentSample, data, data.bytesLeft());
if (havePendingSample()) {
appendSampleData(data, data.bytesLeft());
}
}
@Override
public void packetFinished() {
addSample(currentSample);
currentSample = null;
}
@Override
public void release() {
super.release();
if (currentSample != null) {
recycle(currentSample);
currentSample = null;
}
commitSample(true);
}
}

View file

@ -16,8 +16,12 @@
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint;
import android.media.MediaExtractor;
import java.util.concurrent.ConcurrentLinkedQueue;
/* package */ abstract class SampleQueue {
@ -34,6 +38,10 @@ import java.util.concurrent.ConcurrentLinkedQueue;
private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs;
// Accessed by only the loading thread (except on release, which shouldn't happen until the
// loading thread has been terminated).
private Sample pendingSample;
protected SampleQueue(SamplePool samplePool) {
this.samplePool = samplePool;
internalQueue = new ConcurrentLinkedQueue<Sample>();
@ -43,6 +51,10 @@ import java.util.concurrent.ConcurrentLinkedQueue;
largestParsedTimestampUs = Long.MIN_VALUE;
}
public boolean isEmpty() {
return peek() == null;
}
public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs;
}
@ -60,34 +72,49 @@ import java.util.concurrent.ConcurrentLinkedQueue;
}
/**
* Removes and returns the next sample from the queue.
* Removes the next sample from the head of the queue, writing it into the provided holder.
* <p>
* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples
* queued prior to the first keyframe are discarded.
*
* @return The next sample from the queue, or null if a sample isn't available.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public Sample poll() {
Sample head = peek();
if (head != null) {
internalQueue.poll();
needKeyframe = false;
lastReadTimeUs = head.timeUs;
@SuppressLint("InlinedApi")
public boolean getSample(SampleHolder holder) {
Sample sample = peek();
if (sample == null) {
return false;
}
return head;
// Write the sample into the holder.
if (holder.data == null || holder.data.capacity() < sample.size) {
holder.replaceBuffer(sample.size);
}
if (holder.data != null) {
holder.data.put(sample.data, 0, sample.size);
}
holder.size = sample.size;
holder.flags = sample.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
holder.timeUs = sample.timeUs;
// Pop and recycle the sample, and update state.
needKeyframe = false;
lastReadTimeUs = sample.timeUs;
internalQueue.poll();
samplePool.recycle(sample);
return true;
}
/**
* Like {@link #poll()}, except the returned sample is not removed from the queue.
* Returns (but does not remove) the next sample in the queue.
*
* @return The next sample from the queue, or null if a sample isn't available.
*/
public Sample peek() {
private Sample peek() {
Sample head = internalQueue.peek();
if (needKeyframe) {
// Peeking discard of samples until we find a keyframe or run out of available samples.
while (head != null && !head.isKeyframe) {
recycle(head);
samplePool.recycle(head);
internalQueue.poll();
head = internalQueue.peek();
}
@ -97,7 +124,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
}
if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) {
// The sample is later than the time this queue is spliced out.
recycle(head);
samplePool.recycle(head);
internalQueue.poll();
return null;
}
@ -112,7 +139,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
public void discardUntil(long timeUs) {
Sample head = peek();
while (head != null && head.timeUs < timeUs) {
recycle(head);
samplePool.recycle(head);
internalQueue.poll();
head = internalQueue.peek();
// We're discarding at least one sample, so any subsequent read will need to start at
@ -125,21 +152,16 @@ import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Clears the queue.
*/
public void release() {
public final void release() {
Sample toRecycle = internalQueue.poll();
while (toRecycle != null) {
recycle(toRecycle);
samplePool.recycle(toRecycle);
toRecycle = internalQueue.poll();
}
}
/**
* Recycles a sample.
*
* @param sample The sample to recycle.
*/
public void recycle(Sample sample) {
samplePool.recycle(sample);
if (pendingSample != null) {
samplePool.recycle(pendingSample);
pendingSample = null;
}
}
/**
@ -177,45 +199,34 @@ import java.util.concurrent.ConcurrentLinkedQueue;
return false;
}
/**
* Obtains a Sample object to use.
*
* @param type The type of the sample.
* @return The sample.
*/
protected Sample getSample(int type) {
return samplePool.get(type);
// Writing side.
protected final boolean havePendingSample() {
return pendingSample != null;
}
/**
* Creates a new Sample and adds it to the queue.
*
* @param type The type of the sample.
* @param buffer The buffer to read sample data.
* @param sampleSize The size of the sample data.
* @param sampleTimeUs The sample time stamp.
* @param isKeyframe True if the sample is a keyframe. False otherwise.
*/
protected void addSample(int type, ParsableByteArray buffer, int sampleSize, long sampleTimeUs,
boolean isKeyframe) {
Sample sample = getSample(type);
addToSample(sample, buffer, sampleSize);
sample.isKeyframe = isKeyframe;
sample.timeUs = sampleTimeUs;
addSample(sample);
protected final Sample getPendingSample() {
return pendingSample;
}
protected void addSample(Sample sample) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
internalQueue.add(sample);
protected final void startSample(int type, long timeUs) {
pendingSample = samplePool.get(type);
pendingSample.timeUs = timeUs;
}
protected void addToSample(Sample sample, ParsableByteArray buffer, int size) {
if (sample.data.length - sample.size < size) {
sample.expand(size - sample.data.length + sample.size);
protected final void appendSampleData(ParsableByteArray buffer, int size) {
if (pendingSample.data.length - pendingSample.size < size) {
pendingSample.expand(size - pendingSample.data.length + pendingSample.size);
}
buffer.readBytes(sample.data, sample.size, size);
sample.size += size;
buffer.readBytes(pendingSample.data, pendingSample.size, size);
pendingSample.size += size;
}
protected final void commitSample(boolean isKeyframe) {
pendingSample.isKeyframe = isKeyframe;
internalQueue.add(pendingSample);
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, pendingSample.timeUs);
pendingSample = null;
}
}

View file

@ -16,12 +16,9 @@
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.mp4.Mp4Util;
import com.google.android.exoplayer.text.eia608.Eia608Parser;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint;
/**
* Parses a SEI data from H.264 frames and extracts samples with closed captions data.
*
@ -30,9 +27,6 @@ import android.annotation.SuppressLint;
*/
/* package */ class SeiReader extends SampleQueue {
// SEI data, used for Closed Captions.
private static final int NAL_UNIT_TYPE_SEI = 6;
private final ParsableByteArray seiBuffer;
public SeiReader(SamplePool samplePool) {
@ -41,20 +35,14 @@ import android.annotation.SuppressLint;
seiBuffer = new ParsableByteArray();
}
@SuppressLint("InlinedApi")
public void read(byte[] data, int length, long pesTimeUs) {
seiBuffer.reset(data, length);
while (seiBuffer.bytesLeft() > 0) {
int currentOffset = seiBuffer.getPosition();
int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI);
if (seiOffset == length) {
return;
}
seiBuffer.skip(seiOffset + 4 - currentOffset);
int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
if (ccDataSize > 0) {
addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true);
}
public void read(byte[] data, int position, long pesTimeUs) {
seiBuffer.reset(data, data.length);
seiBuffer.setPosition(position + 4);
int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
if (ccDataSize > 0) {
startSample(Sample.TYPE_MISC, pesTimeUs);
appendSampleData(seiBuffer, ccDataSize);
commitSample(true);
}
}

View file

@ -23,8 +23,6 @@ import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint;
import android.media.MediaExtractor;
import android.util.Log;
import android.util.SparseArray;
@ -172,19 +170,12 @@ public final class TsExtractor {
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param out A {@link SampleHolder} into which the next sample should be read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(int track, SampleHolder out) {
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
SampleQueue sampleQueue = sampleQueues.valueAt(track);
Sample sample = sampleQueue.poll();
if (sample == null) {
return false;
}
convert(sample, out);
sampleQueue.recycle(sample);
return true;
return sampleQueues.valueAt(track).getSample(holder);
}
/**
@ -207,7 +198,7 @@ public final class TsExtractor {
*/
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).peek() != null;
return !sampleQueues.valueAt(track).isEmpty();
}
private boolean checkPrepared() {
@ -284,19 +275,6 @@ public final class TsExtractor {
return bytesRead;
}
@SuppressLint("InlinedApi")
private void convert(Sample in, SampleHolder out) {
if (out.data == null || out.data.capacity() < in.size) {
out.replaceBuffer(in.size);
}
if (out.data != null) {
out.data.put(in.data, 0, in.size);
}
out.size = in.size;
out.flags = in.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
out.timeUs = in.timeUs;
}
/**
* Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound.
*

View file

@ -144,4 +144,15 @@ public final class Mp4Util {
return endOffset;
}
/**
* Gets the type of the NAL unit in {@code data} that starts at {@code offset}.
*
* @param data The data to search.
* @param offset The start offset of a NAL unit.
* @return The type of the unit.
*/
public static int getNalUnitType(byte[] data, int offset) {
return data[offset + 3] & 0x1F;
}
}