Commit 066334da by Oliver Woodman

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
parent 61a86295
...@@ -53,7 +53,6 @@ import java.util.Collections; ...@@ -53,7 +53,6 @@ import java.util.Collections;
// Used when reading the samples. // Used when reading the samples.
private long timeUs; private long timeUs;
private Sample currentSample;
public AdtsReader(SamplePool samplePool) { public AdtsReader(SamplePool samplePool) {
super(samplePool); super(samplePool);
...@@ -78,20 +77,17 @@ import java.util.Collections; ...@@ -78,20 +77,17 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) { if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader(); parseHeader();
currentSample = getSample(Sample.TYPE_AUDIO); startSample(Sample.TYPE_AUDIO, timeUs);
currentSample.timeUs = timeUs;
currentSample.isKeyframe = true;
bytesRead = 0; bytesRead = 0;
state = STATE_READING_SAMPLE; state = STATE_READING_SAMPLE;
} }
break; break;
case STATE_READING_SAMPLE: case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
addToSample(currentSample, data, bytesToRead); appendSampleData(data, bytesToRead);
bytesRead += bytesToRead; bytesRead += bytesToRead;
if (bytesRead == sampleSize) { if (bytesRead == sampleSize) {
addSample(currentSample); commitSample(true);
currentSample = null;
timeUs += frameDurationUs; timeUs += frameDurationUs;
bytesRead = 0; bytesRead = 0;
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
...@@ -106,15 +102,6 @@ import java.util.Collections; ...@@ -106,15 +102,6 @@ import java.util.Collections;
// Do nothing. // 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 * 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. * that the data should be written into {@code target} starting from an offset of zero.
......
...@@ -30,14 +30,13 @@ import java.util.List; ...@@ -30,14 +30,13 @@ import java.util.List;
/* package */ class H264Reader extends PesPayloadReader { /* package */ class H264Reader extends PesPayloadReader {
private static final int NAL_UNIT_TYPE_IDR = 5; 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_SPS = 7;
private static final int NAL_UNIT_TYPE_PPS = 8; private static final int NAL_UNIT_TYPE_PPS = 8;
private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_AUD = 9;
private final SeiReader seiReader; private final SeiReader seiReader;
private Sample currentSample;
public H264Reader(SamplePool samplePool, SeiReader seiReader) { public H264Reader(SamplePool samplePool, SeiReader seiReader) {
super(samplePool); super(samplePool);
this.seiReader = seiReader; this.seiReader = seiReader;
...@@ -47,14 +46,32 @@ import java.util.List; ...@@ -47,14 +46,32 @@ import java.util.List;
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
if (readToNextAudUnit(data, pesTimeUs)) { if (readToNextAudUnit(data, pesTimeUs)) {
currentSample.isKeyframe = currentSample.size // TODO: Allowing access to the Sample object here is messy. Fix this.
> Mp4Util.findNalUnit(currentSample.data, 0, currentSample.size, NAL_UNIT_TYPE_IDR); Sample pendingSample = getPendingSample();
if (!hasMediaFormat() && currentSample.isKeyframe) { byte[] pendingSampleData = pendingSample.data;
parseMediaFormat(currentSample); 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;
}
}
boolean isKeyframe = pendingSampleSize > idrNalUnitPosition;
if (!hasMediaFormat() && isKeyframe) {
parseMediaFormat(pendingSampleData, pendingSampleSize);
} }
seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); commitSample(isKeyframe);
addSample(currentSample);
currentSample = null;
} }
} }
} }
...@@ -64,15 +81,6 @@ import java.util.List; ...@@ -64,15 +81,6 @@ import java.util.List;
// Do nothing. // 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. * Reads data up to (but not including) the start of the next AUD unit.
* *
...@@ -89,16 +97,15 @@ import java.util.List; ...@@ -89,16 +97,15 @@ import java.util.List;
int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD);
int bytesToNextAud = audOffset - pesOffset; int bytesToNextAud = audOffset - pesOffset;
if (bytesToNextAud == 0) { if (bytesToNextAud == 0) {
if (currentSample == null) { if (!havePendingSample()) {
currentSample = getSample(Sample.TYPE_VIDEO); startSample(Sample.TYPE_VIDEO, pesTimeUs);
currentSample.timeUs = pesTimeUs; appendSampleData(data, 4);
addToSample(currentSample, data, 4);
return false; return false;
} else { } else {
return true; return true;
} }
} else if (currentSample != null) { } else if (havePendingSample()) {
addToSample(currentSample, data, bytesToNextAud); appendSampleData(data, bytesToNextAud);
return data.bytesLeft() > 0; return data.bytesLeft() > 0;
} else { } else {
data.skip(bytesToNextAud); data.skip(bytesToNextAud);
...@@ -106,9 +113,7 @@ import java.util.List; ...@@ -106,9 +113,7 @@ import java.util.List;
} }
} }
private void parseMediaFormat(Sample sample) { private void parseMediaFormat(byte[] sampleData, int sampleSize) {
byte[] sampleData = sample.data;
int sampleSize = sample.size;
// Locate the SPS and PPS units. // Locate the SPS and PPS units.
int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS); int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS);
int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS); int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS);
......
...@@ -25,8 +25,6 @@ import android.annotation.SuppressLint; ...@@ -25,8 +25,6 @@ import android.annotation.SuppressLint;
*/ */
/* package */ class Id3Reader extends PesPayloadReader { /* package */ class Id3Reader extends PesPayloadReader {
private Sample currentSample;
public Id3Reader(SamplePool samplePool) { public Id3Reader(SamplePool samplePool) {
super(samplePool); super(samplePool);
setMediaFormat(MediaFormat.createId3Format()); setMediaFormat(MediaFormat.createId3Format());
...@@ -36,28 +34,16 @@ import android.annotation.SuppressLint; ...@@ -36,28 +34,16 @@ import android.annotation.SuppressLint;
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) { if (startOfPacket) {
currentSample = getSample(Sample.TYPE_MISC); startSample(Sample.TYPE_MISC, pesTimeUs);
currentSample.timeUs = pesTimeUs;
currentSample.isKeyframe = true;
} }
if (currentSample != null) { if (havePendingSample()) {
addToSample(currentSample, data, data.bytesLeft()); appendSampleData(data, data.bytesLeft());
} }
} }
@Override @Override
public void packetFinished() { public void packetFinished() {
addSample(currentSample); commitSample(true);
currentSample = null;
}
@Override
public void release() {
super.release();
if (currentSample != null) {
recycle(currentSample);
currentSample = null;
}
} }
} }
...@@ -16,8 +16,12 @@ ...@@ -16,8 +16,12 @@
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint;
import android.media.MediaExtractor;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
/* package */ abstract class SampleQueue { /* package */ abstract class SampleQueue {
...@@ -34,6 +38,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -34,6 +38,10 @@ import java.util.concurrent.ConcurrentLinkedQueue;
private volatile MediaFormat mediaFormat; private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs; 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) { protected SampleQueue(SamplePool samplePool) {
this.samplePool = samplePool; this.samplePool = samplePool;
internalQueue = new ConcurrentLinkedQueue<Sample>(); internalQueue = new ConcurrentLinkedQueue<Sample>();
...@@ -43,6 +51,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -43,6 +51,10 @@ import java.util.concurrent.ConcurrentLinkedQueue;
largestParsedTimestampUs = Long.MIN_VALUE; largestParsedTimestampUs = Long.MIN_VALUE;
} }
public boolean isEmpty() {
return peek() == null;
}
public long getLargestParsedTimestampUs() { public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs; return largestParsedTimestampUs;
} }
...@@ -60,34 +72,49 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -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> * <p>
* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples
* queued prior to the first keyframe are discarded. * 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() { @SuppressLint("InlinedApi")
Sample head = peek(); public boolean getSample(SampleHolder holder) {
if (head != null) { Sample sample = peek();
internalQueue.poll(); if (sample == null) {
needKeyframe = false; return false;
lastReadTimeUs = head.timeUs;
} }
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. * @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(); Sample head = internalQueue.peek();
if (needKeyframe) { if (needKeyframe) {
// Peeking discard of samples until we find a keyframe or run out of available samples. // Peeking discard of samples until we find a keyframe or run out of available samples.
while (head != null && !head.isKeyframe) { while (head != null && !head.isKeyframe) {
recycle(head); samplePool.recycle(head);
internalQueue.poll(); internalQueue.poll();
head = internalQueue.peek(); head = internalQueue.peek();
} }
...@@ -97,7 +124,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -97,7 +124,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
} }
if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) { if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) {
// The sample is later than the time this queue is spliced out. // The sample is later than the time this queue is spliced out.
recycle(head); samplePool.recycle(head);
internalQueue.poll(); internalQueue.poll();
return null; return null;
} }
...@@ -112,7 +139,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -112,7 +139,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
public void discardUntil(long timeUs) { public void discardUntil(long timeUs) {
Sample head = peek(); Sample head = peek();
while (head != null && head.timeUs < timeUs) { while (head != null && head.timeUs < timeUs) {
recycle(head); samplePool.recycle(head);
internalQueue.poll(); internalQueue.poll();
head = internalQueue.peek(); head = internalQueue.peek();
// We're discarding at least one sample, so any subsequent read will need to start at // 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; ...@@ -125,21 +152,16 @@ import java.util.concurrent.ConcurrentLinkedQueue;
/** /**
* Clears the queue. * Clears the queue.
*/ */
public void release() { public final void release() {
Sample toRecycle = internalQueue.poll(); Sample toRecycle = internalQueue.poll();
while (toRecycle != null) { while (toRecycle != null) {
recycle(toRecycle); samplePool.recycle(toRecycle);
toRecycle = internalQueue.poll(); toRecycle = internalQueue.poll();
} }
} if (pendingSample != null) {
samplePool.recycle(pendingSample);
/** pendingSample = null;
* Recycles a sample. }
*
* @param sample The sample to recycle.
*/
public void recycle(Sample sample) {
samplePool.recycle(sample);
} }
/** /**
...@@ -177,45 +199,34 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -177,45 +199,34 @@ import java.util.concurrent.ConcurrentLinkedQueue;
return false; return false;
} }
/** // Writing side.
* Obtains a Sample object to use.
* protected final boolean havePendingSample() {
* @param type The type of the sample. return pendingSample != null;
* @return The sample.
*/
protected Sample getSample(int type) {
return samplePool.get(type);
} }
/** protected final Sample getPendingSample() {
* Creates a new Sample and adds it to the queue. return pendingSample;
*
* @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 void addSample(Sample sample) { protected final void startSample(int type, long timeUs) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); pendingSample = samplePool.get(type);
internalQueue.add(sample); pendingSample.timeUs = timeUs;
} }
protected void addToSample(Sample sample, ParsableByteArray buffer, int size) { protected final void appendSampleData(ParsableByteArray buffer, int size) {
if (sample.data.length - sample.size < size) { if (pendingSample.data.length - pendingSample.size < size) {
sample.expand(size - sample.data.length + sample.size); pendingSample.expand(size - pendingSample.data.length + pendingSample.size);
} }
buffer.readBytes(sample.data, sample.size, size); buffer.readBytes(pendingSample.data, pendingSample.size, size);
sample.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;
} }
} }
...@@ -16,12 +16,9 @@ ...@@ -16,12 +16,9 @@
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; 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.text.eia608.Eia608Parser;
import com.google.android.exoplayer.util.ParsableByteArray; 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. * Parses a SEI data from H.264 frames and extracts samples with closed captions data.
* *
...@@ -30,9 +27,6 @@ import android.annotation.SuppressLint; ...@@ -30,9 +27,6 @@ import android.annotation.SuppressLint;
*/ */
/* package */ class SeiReader extends SampleQueue { /* package */ class SeiReader extends SampleQueue {
// SEI data, used for Closed Captions.
private static final int NAL_UNIT_TYPE_SEI = 6;
private final ParsableByteArray seiBuffer; private final ParsableByteArray seiBuffer;
public SeiReader(SamplePool samplePool) { public SeiReader(SamplePool samplePool) {
...@@ -41,20 +35,14 @@ import android.annotation.SuppressLint; ...@@ -41,20 +35,14 @@ import android.annotation.SuppressLint;
seiBuffer = new ParsableByteArray(); seiBuffer = new ParsableByteArray();
} }
@SuppressLint("InlinedApi") public void read(byte[] data, int position, long pesTimeUs) {
public void read(byte[] data, int length, long pesTimeUs) { seiBuffer.reset(data, data.length);
seiBuffer.reset(data, length); seiBuffer.setPosition(position + 4);
while (seiBuffer.bytesLeft() > 0) { int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
int currentOffset = seiBuffer.getPosition(); if (ccDataSize > 0) {
int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI); startSample(Sample.TYPE_MISC, pesTimeUs);
if (seiOffset == length) { appendSampleData(seiBuffer, ccDataSize);
return; commitSample(true);
}
seiBuffer.skip(seiOffset + 4 - currentOffset);
int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
if (ccDataSize > 0) {
addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true);
}
} }
} }
......
...@@ -23,8 +23,6 @@ import com.google.android.exoplayer.util.Assertions; ...@@ -23,8 +23,6 @@ import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint;
import android.media.MediaExtractor;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
...@@ -172,19 +170,12 @@ public final class TsExtractor { ...@@ -172,19 +170,12 @@ public final class TsExtractor {
* Gets the next sample for the specified track. * Gets the next sample for the specified track.
* *
* @param track The track from which to read. * @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. * @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); Assertions.checkState(prepared);
SampleQueue sampleQueue = sampleQueues.valueAt(track); return sampleQueues.valueAt(track).getSample(holder);
Sample sample = sampleQueue.poll();
if (sample == null) {
return false;
}
convert(sample, out);
sampleQueue.recycle(sample);
return true;
} }
/** /**
...@@ -207,7 +198,7 @@ public final class TsExtractor { ...@@ -207,7 +198,7 @@ public final class TsExtractor {
*/ */
public boolean hasSamples(int track) { public boolean hasSamples(int track) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
return sampleQueues.valueAt(track).peek() != null; return !sampleQueues.valueAt(track).isEmpty();
} }
private boolean checkPrepared() { private boolean checkPrepared() {
...@@ -284,19 +275,6 @@ public final class TsExtractor { ...@@ -284,19 +275,6 @@ public final class TsExtractor {
return bytesRead; 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. * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound.
* *
......
...@@ -144,4 +144,15 @@ public final class Mp4Util { ...@@ -144,4 +144,15 @@ public final class Mp4Util {
return endOffset; 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;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment