diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java index 7bea5b17c1..41b2e35b62 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java @@ -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. diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java index 6bf1d5c4f5..2c78a0487b 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java @@ -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); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java index b30aead7d4..a579b47855 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java @@ -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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java index 1a8623468f..a4d418c716 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java @@ -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(); @@ -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. *

* 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; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java index 6d98c50a6d..a86b44a18d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java @@ -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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java index 5f698fd6a5..9c174755c1 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -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. * diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index 09abe2ccf1..0cc6d4d65e 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -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; + } + }