mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
commit
ccac9fad4e
17 changed files with 731 additions and 135 deletions
|
|
@ -25,7 +25,6 @@ import android.media.AudioFormat;
|
|||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Renders encoded AC-3/enhanced AC-3 data to an {@link AudioTrack} for decoding on the playback
|
||||
|
|
@ -105,8 +104,8 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
|
|||
this.source = Assertions.checkNotNull(source);
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
sampleHolder.data = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
|
||||
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
sampleHolder.replaceBuffer(DEFAULT_BUFFER_SIZE);
|
||||
formatHolder = new MediaFormatHolder();
|
||||
audioTrack = new AudioTrack();
|
||||
shouldReadInputBuffer = true;
|
||||
|
|
@ -199,8 +198,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
|
|||
|
||||
// Get more data if we have run out.
|
||||
if (shouldReadInputBuffer) {
|
||||
sampleHolder.data.clear();
|
||||
|
||||
sampleHolder.clearData();
|
||||
int result =
|
||||
source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.FORMAT_READ) {
|
||||
|
|
|
|||
|
|
@ -96,4 +96,13 @@ public final class SampleHolder {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears {@link #data}. Does nothing if {@link #data} is null.
|
||||
*/
|
||||
public void clearData() {
|
||||
if (data != null) {
|
||||
data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
parsedAtoms.add(Atom.TYPE_moof);
|
||||
parsedAtoms.add(Atom.TYPE_moov);
|
||||
parsedAtoms.add(Atom.TYPE_mp4a);
|
||||
parsedAtoms.add(Atom.TYPE_mvhd);
|
||||
parsedAtoms.add(Atom.TYPE_sidx);
|
||||
parsedAtoms.add(Atom.TYPE_stsd);
|
||||
parsedAtoms.add(Atom.TYPE_tfdt);
|
||||
|
|
@ -379,7 +380,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
}
|
||||
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
||||
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
|
||||
track = CommonMp4AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak));
|
||||
track = CommonMp4AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
|
||||
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||
}
|
||||
|
||||
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ import android.util.SparseArray;
|
|||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
|
|
@ -541,7 +543,6 @@ public final class TsExtractor {
|
|||
|
||||
@SuppressWarnings("hiding")
|
||||
private final SamplePool samplePool;
|
||||
private final ConcurrentLinkedQueue<Sample> internalQueue;
|
||||
|
||||
// Accessed only by the consuming thread.
|
||||
private boolean needKeyframe;
|
||||
|
|
@ -553,7 +554,6 @@ public final class TsExtractor {
|
|||
|
||||
protected SampleQueue(SamplePool samplePool) {
|
||||
this.samplePool = samplePool;
|
||||
internalQueue = new ConcurrentLinkedQueue<Sample>();
|
||||
needKeyframe = true;
|
||||
lastReadTimeUs = Long.MIN_VALUE;
|
||||
spliceOutTimeUs = Long.MIN_VALUE;
|
||||
|
|
@ -582,7 +582,7 @@ public final class TsExtractor {
|
|||
public Sample poll() {
|
||||
Sample head = peek();
|
||||
if (head != null) {
|
||||
internalQueue.remove();
|
||||
internalPollSample();
|
||||
needKeyframe = false;
|
||||
lastReadTimeUs = head.timeUs;
|
||||
}
|
||||
|
|
@ -595,13 +595,13 @@ public final class TsExtractor {
|
|||
* @return The next sample from the queue, or null if a sample isn't available.
|
||||
*/
|
||||
public Sample peek() {
|
||||
Sample head = internalQueue.peek();
|
||||
Sample head = internalPeekSample();
|
||||
if (needKeyframe) {
|
||||
// Peeking discard of samples until we find a keyframe or run out of available samples.
|
||||
while (head != null && !head.isKeyframe) {
|
||||
recycle(head);
|
||||
internalQueue.remove();
|
||||
head = internalQueue.peek();
|
||||
internalPollSample();
|
||||
head = internalPeekSample();
|
||||
}
|
||||
}
|
||||
if (head == null) {
|
||||
|
|
@ -610,7 +610,7 @@ public final class TsExtractor {
|
|||
if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) {
|
||||
// The sample is later than the time this queue is spliced out.
|
||||
recycle(head);
|
||||
internalQueue.remove();
|
||||
internalPollSample();
|
||||
return null;
|
||||
}
|
||||
return head;
|
||||
|
|
@ -625,8 +625,8 @@ public final class TsExtractor {
|
|||
Sample head = peek();
|
||||
while (head != null && head.timeUs < timeUs) {
|
||||
recycle(head);
|
||||
internalQueue.remove();
|
||||
head = internalQueue.peek();
|
||||
internalPollSample();
|
||||
head = internalPeekSample();
|
||||
// We're discarding at least one sample, so any subsequent read will need to start at
|
||||
// a keyframe.
|
||||
needKeyframe = true;
|
||||
|
|
@ -638,10 +638,10 @@ public final class TsExtractor {
|
|||
* Clears the queue.
|
||||
*/
|
||||
public void release() {
|
||||
Sample toRecycle = internalQueue.poll();
|
||||
Sample toRecycle = internalPollSample();
|
||||
while (toRecycle != null) {
|
||||
recycle(toRecycle);
|
||||
toRecycle = internalQueue.poll();
|
||||
toRecycle = internalPollSample();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -666,20 +666,19 @@ public final class TsExtractor {
|
|||
return true;
|
||||
}
|
||||
long firstPossibleSpliceTime;
|
||||
Sample nextSample = internalQueue.peek();
|
||||
Sample nextSample = internalPeekSample();
|
||||
if (nextSample != null) {
|
||||
firstPossibleSpliceTime = nextSample.timeUs;
|
||||
} else {
|
||||
firstPossibleSpliceTime = lastReadTimeUs + 1;
|
||||
}
|
||||
ConcurrentLinkedQueue<Sample> nextInternalQueue = nextQueue.internalQueue;
|
||||
Sample nextQueueSample = nextInternalQueue.peek();
|
||||
Sample nextQueueSample = nextQueue.internalPeekSample();
|
||||
while (nextQueueSample != null
|
||||
&& (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) {
|
||||
// Discard samples from the next queue for as long as they are before the earliest possible
|
||||
// splice time, or not keyframes.
|
||||
nextQueue.internalQueue.remove();
|
||||
nextQueueSample = nextQueue.internalQueue.peek();
|
||||
nextQueue.internalPollSample();
|
||||
nextQueueSample = nextQueue.internalPeekSample();
|
||||
}
|
||||
if (nextQueueSample != null) {
|
||||
// We've found a keyframe in the next queue that can serve as the splice point. Set the
|
||||
|
|
@ -720,7 +719,7 @@ public final class TsExtractor {
|
|||
|
||||
protected void addSample(Sample sample) {
|
||||
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
|
||||
internalQueue.add(sample);
|
||||
internalQueueSample(sample);
|
||||
}
|
||||
|
||||
protected void addToSample(Sample sample, BitArray buffer, int size) {
|
||||
|
|
@ -731,15 +730,37 @@ public final class TsExtractor {
|
|||
sample.size += size;
|
||||
}
|
||||
|
||||
protected abstract Sample internalPeekSample();
|
||||
protected abstract Sample internalPollSample();
|
||||
protected abstract void internalQueueSample(Sample sample);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts individual samples from continuous byte stream.
|
||||
* Extracts individual samples from continuous byte stream, preserving original order.
|
||||
*/
|
||||
private abstract class PesPayloadReader extends SampleQueue {
|
||||
|
||||
private final ConcurrentLinkedQueue<Sample> internalQueue;
|
||||
|
||||
protected PesPayloadReader(SamplePool samplePool) {
|
||||
super(samplePool);
|
||||
internalQueue = new ConcurrentLinkedQueue<Sample>();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final Sample internalPeekSample() {
|
||||
return internalQueue.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final Sample internalPollSample() {
|
||||
return internalQueue.poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void internalQueueSample(Sample sample) {
|
||||
internalQueue.add(sample);
|
||||
}
|
||||
|
||||
public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs);
|
||||
|
|
@ -992,18 +1013,23 @@ public final class TsExtractor {
|
|||
|
||||
/**
|
||||
* Parses a SEI data from H.264 frames and extracts samples with closed captions data.
|
||||
*
|
||||
* TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that
|
||||
* a sample with an earlier timestamp won't be added to it.
|
||||
*/
|
||||
private class SeiReader extends SampleQueue {
|
||||
private class SeiReader extends SampleQueue implements Comparator<Sample> {
|
||||
|
||||
// SEI data, used for Closed Captions.
|
||||
private static final int NAL_UNIT_TYPE_SEI = 6;
|
||||
|
||||
private final BitArray seiBuffer;
|
||||
private final TreeSet<Sample> internalQueue;
|
||||
|
||||
public SeiReader(SamplePool samplePool) {
|
||||
super(samplePool);
|
||||
setMediaFormat(MediaFormat.createEia608Format());
|
||||
seiBuffer = new BitArray();
|
||||
internalQueue = new TreeSet<Sample>(this);
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
|
|
@ -1022,6 +1048,27 @@ public final class TsExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Sample first, Sample second) {
|
||||
// Note - We don't expect samples to have identical timestamps.
|
||||
return first.timeUs <= second.timeUs ? -1 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized Sample internalPeekSample() {
|
||||
return internalQueue.isEmpty() ? null : internalQueue.first();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized Sample internalPollSample() {
|
||||
return internalQueue.pollFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void internalQueueSample(Sample sample) {
|
||||
internalQueue.add(sample);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ public abstract class Atom {
|
|||
public static final int TYPE_trun = getAtomTypeInteger("trun");
|
||||
public static final int TYPE_sidx = getAtomTypeInteger("sidx");
|
||||
public static final int TYPE_moov = getAtomTypeInteger("moov");
|
||||
public static final int TYPE_mvhd = getAtomTypeInteger("mvhd");
|
||||
public static final int TYPE_trak = getAtomTypeInteger("trak");
|
||||
public static final int TYPE_mdia = getAtomTypeInteger("mdia");
|
||||
public static final int TYPE_minf = getAtomTypeInteger("minf");
|
||||
|
|
@ -69,6 +70,7 @@ public abstract class Atom {
|
|||
public static final int TYPE_mp4v = getAtomTypeInteger("mp4v");
|
||||
public static final int TYPE_stts = getAtomTypeInteger("stts");
|
||||
public static final int TYPE_stss = getAtomTypeInteger("stss");
|
||||
public static final int TYPE_ctts = getAtomTypeInteger("ctts");
|
||||
public static final int TYPE_stsc = getAtomTypeInteger("stsc");
|
||||
public static final int TYPE_stsz = getAtomTypeInteger("stsz");
|
||||
public static final int TYPE_stco = getAtomTypeInteger("stco");
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import com.google.android.exoplayer.util.MimeTypes;
|
|||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaExtractor;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -40,11 +42,13 @@ public final class CommonMp4AtomParsers {
|
|||
192, 224, 256, 320, 384, 448, 512, 576, 640};
|
||||
|
||||
/**
|
||||
* Parses a trak atom (defined in 14496-12).
|
||||
* Parses a trak atom (defined in 14496-12)
|
||||
*
|
||||
* @param trak Atom to parse.
|
||||
* @param mvhd Movie header atom, used to get the timescale.
|
||||
* @return A {@link Track} instance.
|
||||
*/
|
||||
public static Track parseTrak(Atom.ContainerAtom trak) {
|
||||
public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd) {
|
||||
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
||||
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
||||
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO
|
||||
|
|
@ -53,22 +57,211 @@ public final class CommonMp4AtomParsers {
|
|||
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
|
||||
int id = header.first;
|
||||
long duration = header.second;
|
||||
long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
|
||||
long movieTimescale = parseMvhd(mvhd.data);
|
||||
long durationUs;
|
||||
if (duration == -1) {
|
||||
durationUs = C.UNKNOWN_TIME_US;
|
||||
} else {
|
||||
durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale);
|
||||
durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);
|
||||
}
|
||||
Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
|
||||
.getContainerAtomOfType(Atom.TYPE_stbl);
|
||||
|
||||
long mediaTimescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
|
||||
Pair<MediaFormat, TrackEncryptionBox[]> sampleDescriptions =
|
||||
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data);
|
||||
return new Track(id, trackType, timescale, durationUs, sampleDescriptions.first,
|
||||
return new Track(id, trackType, mediaTimescale, durationUs, sampleDescriptions.first,
|
||||
sampleDescriptions.second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an stbl atom (defined in 14496-12).
|
||||
*
|
||||
* @param track Track to which this sample table corresponds.
|
||||
* @param stblAtom stbl (sample table) atom to parse.
|
||||
* @return Sample table described by the stbl atom.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
public static Mp4TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) {
|
||||
// Array of sample sizes.
|
||||
ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;
|
||||
|
||||
// Entries are byte offsets of chunks.
|
||||
ParsableByteArray chunkOffsets;
|
||||
Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
|
||||
if (chunkOffsetsAtom == null) {
|
||||
chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64);
|
||||
}
|
||||
chunkOffsets = chunkOffsetsAtom.data;
|
||||
// Entries are (chunk number, number of samples per chunk, sample description index).
|
||||
ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data;
|
||||
// Entries are (number of samples, timestamp delta between those samples).
|
||||
ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data;
|
||||
// Entries are the indices of samples that are synchronization samples.
|
||||
Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
|
||||
ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
|
||||
// Entries are (number of samples, timestamp offset).
|
||||
Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
|
||||
ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
|
||||
|
||||
// Skip full atom.
|
||||
stsz.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
|
||||
int fixedSampleSize = stsz.readUnsignedIntToInt();
|
||||
int sampleCount = stsz.readUnsignedIntToInt();
|
||||
|
||||
int[] sizes = new int[sampleCount];
|
||||
long[] timestamps = new long[sampleCount];
|
||||
long[] offsets = new long[sampleCount];
|
||||
int[] flags = new int[sampleCount];
|
||||
|
||||
// Prepare to read chunk offsets.
|
||||
chunkOffsets.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
|
||||
int chunkCount = chunkOffsets.readUnsignedIntToInt();
|
||||
|
||||
stsc.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
|
||||
int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1;
|
||||
Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1");
|
||||
int samplesPerChunk = stsc.readUnsignedIntToInt();
|
||||
stsc.skip(4); // Skip the sample description index.
|
||||
int nextSamplesPerChunkChangeChunkIndex = -1;
|
||||
if (remainingSamplesPerChunkChanges > 0) {
|
||||
// Store the chunk index when the samples-per-chunk will next change.
|
||||
nextSamplesPerChunkChangeChunkIndex = stsc.readUnsignedIntToInt() - 1;
|
||||
}
|
||||
|
||||
int chunkIndex = 0;
|
||||
int remainingSamplesInChunk = samplesPerChunk;
|
||||
|
||||
// Prepare to read sample timestamps.
|
||||
stts.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
|
||||
int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
|
||||
int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
|
||||
int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
|
||||
|
||||
// Prepare to read sample timestamp offsets, if ctts is present.
|
||||
boolean cttsHasSignedOffsets = false;
|
||||
int remainingSamplesAtTimestampOffset = 0;
|
||||
int remainingTimestampOffsetChanges = 0;
|
||||
int timestampOffset = 0;
|
||||
if (ctts != null) {
|
||||
ctts.setPosition(Mp4Util.ATOM_HEADER_SIZE);
|
||||
cttsHasSignedOffsets = Mp4Util.parseFullAtomVersion(ctts.readInt()) == 1;
|
||||
remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1;
|
||||
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
|
||||
timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt();
|
||||
}
|
||||
|
||||
int nextSynchronizationSampleIndex = -1;
|
||||
int remainingSynchronizationSamples = 0;
|
||||
if (stss != null) {
|
||||
stss.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
|
||||
remainingSynchronizationSamples = stss.readUnsignedIntToInt();
|
||||
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
|
||||
}
|
||||
|
||||
// Calculate the chunk offsets
|
||||
long offsetBytes;
|
||||
if (chunkOffsetsAtom.type == Atom.TYPE_stco) {
|
||||
offsetBytes = chunkOffsets.readUnsignedInt();
|
||||
} else {
|
||||
offsetBytes = chunkOffsets.readUnsignedLongToLong();
|
||||
}
|
||||
|
||||
long timestampTimeUnits = 0;
|
||||
for (int i = 0; i < sampleCount; i++) {
|
||||
offsets[i] = offsetBytes;
|
||||
sizes[i] = fixedSampleSize == 0 ? stsz.readUnsignedIntToInt() : fixedSampleSize;
|
||||
timestamps[i] = timestampTimeUnits + timestampOffset;
|
||||
|
||||
// All samples are synchronization samples if the stss is not present.
|
||||
flags[i] = stss == null ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
||||
if (i == nextSynchronizationSampleIndex) {
|
||||
flags[i] = MediaExtractor.SAMPLE_FLAG_SYNC;
|
||||
remainingSynchronizationSamples--;
|
||||
if (remainingSynchronizationSamples > 0) {
|
||||
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Add on the duration of this sample.
|
||||
timestampTimeUnits += timestampDeltaInTimeUnits;
|
||||
remainingSamplesAtTimestampDelta--;
|
||||
if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
|
||||
remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
|
||||
timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
|
||||
remainingTimestampDeltaChanges--;
|
||||
}
|
||||
|
||||
// Add on the timestamp offset if ctts is present.
|
||||
if (ctts != null) {
|
||||
remainingSamplesAtTimestampOffset--;
|
||||
if (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
|
||||
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
|
||||
timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt();
|
||||
remainingTimestampOffsetChanges--;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're at the last sample in this chunk, move to the next chunk.
|
||||
remainingSamplesInChunk--;
|
||||
if (remainingSamplesInChunk == 0) {
|
||||
chunkIndex++;
|
||||
if (chunkIndex < chunkCount) {
|
||||
if (chunkOffsetsAtom.type == Atom.TYPE_stco) {
|
||||
offsetBytes = chunkOffsets.readUnsignedInt();
|
||||
} else {
|
||||
offsetBytes = chunkOffsets.readUnsignedLongToLong();
|
||||
}
|
||||
}
|
||||
|
||||
// Change the samples-per-chunk if required.
|
||||
if (chunkIndex == nextSamplesPerChunkChangeChunkIndex) {
|
||||
samplesPerChunk = stsc.readUnsignedIntToInt();
|
||||
stsc.skip(4); // Skip the sample description index.
|
||||
remainingSamplesPerChunkChanges--;
|
||||
if (remainingSamplesPerChunkChanges > 0) {
|
||||
nextSamplesPerChunkChangeChunkIndex = stsc.readUnsignedIntToInt() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Expect samplesPerChunk samples in the following chunk, if it's before the end.
|
||||
if (chunkIndex < chunkCount) {
|
||||
remainingSamplesInChunk = samplesPerChunk;
|
||||
}
|
||||
} else {
|
||||
// The next sample follows the current one.
|
||||
offsetBytes += sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
Util.scaleLargeTimestampsInPlace(timestamps, 1000000, track.timescale);
|
||||
|
||||
// Check all the expected samples have been seen.
|
||||
Assertions.checkArgument(remainingSynchronizationSamples == 0);
|
||||
Assertions.checkArgument(remainingSamplesAtTimestampDelta == 0);
|
||||
Assertions.checkArgument(remainingSamplesInChunk == 0);
|
||||
Assertions.checkArgument(remainingTimestampDeltaChanges == 0);
|
||||
Assertions.checkArgument(remainingTimestampOffsetChanges == 0);
|
||||
return new Mp4TrackSampleTable(offsets, sizes, timestamps, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.
|
||||
*
|
||||
* @param mvhd Contents of the mvhd atom to be parsed.
|
||||
* @return Timescale for the movie.
|
||||
*/
|
||||
private static long parseMvhd(ParsableByteArray mvhd) {
|
||||
mvhd.setPosition(Mp4Util.ATOM_HEADER_SIZE);
|
||||
|
||||
int fullAtom = mvhd.readInt();
|
||||
int version = Mp4Util.parseFullAtomVersion(fullAtom);
|
||||
|
||||
mvhd.skip(version == 0 ? 8 : 16);
|
||||
|
||||
return mvhd.readUnsignedInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a tkhd atom (defined in 14496-12).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (C) 2014 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.exoplayer.mp4;
|
||||
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
/** Sample table for a track in an MP4 file. */
|
||||
public final class Mp4TrackSampleTable {
|
||||
|
||||
/** Sample offsets in bytes. */
|
||||
public final long[] offsets;
|
||||
/** Sample sizes in bytes. */
|
||||
public final int[] sizes;
|
||||
/** Sample timestamps in microseconds. */
|
||||
public final long[] timestampsUs;
|
||||
/** Sample flags. */
|
||||
public final int[] flags;
|
||||
|
||||
Mp4TrackSampleTable(
|
||||
long[] offsets, int[] sizes, long[] timestampsUs, int[] flags) {
|
||||
Assertions.checkArgument(sizes.length == timestampsUs.length);
|
||||
Assertions.checkArgument(offsets.length == timestampsUs.length);
|
||||
Assertions.checkArgument(flags.length == timestampsUs.length);
|
||||
|
||||
this.offsets = offsets;
|
||||
this.sizes = sizes;
|
||||
this.timestampsUs = timestampsUs;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
/** Returns the number of samples in the table. */
|
||||
public int getSampleCount() {
|
||||
return sizes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample index of the closest synchronization sample at or before the given
|
||||
* timestamp, if one is available.
|
||||
*
|
||||
* @param timeUs Timestamp adjacent to which to find a synchronization sample.
|
||||
* @return Index of the synchronization sample, or {@link Mp4Util#NO_SAMPLE} if none.
|
||||
*/
|
||||
public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
|
||||
int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
|
||||
for (int i = startIndex; i >= 0; i--) {
|
||||
if (timestampsUs[i] <= timeUs && (flags[i] & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return Mp4Util.NO_SAMPLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample index of the closest synchronization sample at or after the given timestamp,
|
||||
* if one is available.
|
||||
*
|
||||
* @param timeUs Timestamp adjacent to which to find a synchronization sample.
|
||||
* @return index Index of the synchronization sample, or {@link Mp4Util#NO_SAMPLE} if none.
|
||||
*/
|
||||
public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
|
||||
int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
|
||||
for (int i = startIndex; i < timestampsUs.length; i++) {
|
||||
if (timestampsUs[i] >= timeUs && (flags[i] & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return Mp4Util.NO_SAMPLE;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -34,6 +34,15 @@ public final class Mp4Util {
|
|||
/** Size of a full atom header, in bytes. */
|
||||
public static final int FULL_ATOM_HEADER_SIZE = 12;
|
||||
|
||||
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
|
||||
public static final int LONG_ATOM_SIZE = 1;
|
||||
|
||||
/** Sample index when no sample is available. */
|
||||
public static final int NO_SAMPLE = -1;
|
||||
|
||||
/** Track index when no track is selected. */
|
||||
public static final int NO_TRACK = -1;
|
||||
|
||||
/** Four initial bytes that must prefix H.264/AVC NAL units for decoding. */
|
||||
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
||||
|
||||
|
|
|
|||
|
|
@ -91,8 +91,9 @@ public interface SampleExtractor {
|
|||
* {@link SampleSource#END_OF_STREAM} if the last samples in all tracks have been read, or
|
||||
* {@link SampleSource#NOTHING_READ} if the sample cannot be read immediately as it is not
|
||||
* loaded.
|
||||
* @throws IOException Thrown if the source can't be read.
|
||||
*/
|
||||
int readSample(int track, SampleHolder sampleHolder);
|
||||
int readSample(int track, SampleHolder sampleHolder) throws IOException;
|
||||
|
||||
/** Releases resources associated with this extractor. */
|
||||
void release();
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@ public class SubtitleParserHelper implements Handler.Callback {
|
|||
if (sampleHolder != holder) {
|
||||
// A flush has occurred since this holder was posted. Do nothing.
|
||||
} else {
|
||||
holder.data.position(0);
|
||||
this.result = result;
|
||||
this.error = error;
|
||||
this.parsing = false;
|
||||
|
|
|
|||
|
|
@ -177,8 +177,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||
if (!inputStreamEnded && subtitle == null) {
|
||||
try {
|
||||
SampleHolder sampleHolder = parserHelper.getSampleHolder();
|
||||
sampleHolder.clearData();
|
||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
if (result == SampleSource.SAMPLE_READ && !sampleHolder.decodeOnly) {
|
||||
parserHelper.startParseOperation();
|
||||
textRendererNeedsUpdate = false;
|
||||
} else if (result == SampleSource.END_OF_STREAM) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608;
|
|||
/**
|
||||
* A Closed Caption that contains textual data associated with time indices.
|
||||
*/
|
||||
public final class ClosedCaption implements Comparable<ClosedCaption> {
|
||||
/* package */ abstract class ClosedCaption implements Comparable<ClosedCaption> {
|
||||
|
||||
/**
|
||||
* Identifies closed captions with control characters.
|
||||
|
|
@ -30,23 +30,16 @@ public final class ClosedCaption implements Comparable<ClosedCaption> {
|
|||
public static final int TYPE_TEXT = 1;
|
||||
|
||||
/**
|
||||
* The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field
|
||||
* has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control
|
||||
* characters (C1, C2).
|
||||
* The type of the closed caption data.
|
||||
*/
|
||||
public final int type;
|
||||
/**
|
||||
* Contains text or two control characters.
|
||||
*/
|
||||
public final String text;
|
||||
/**
|
||||
* Timestamp associated with the closed caption.
|
||||
*/
|
||||
public final long timeUs;
|
||||
|
||||
public ClosedCaption(int type, String text, long timeUs) {
|
||||
protected ClosedCaption(int type, long timeUs) {
|
||||
this.type = type;
|
||||
this.text = text;
|
||||
this.timeUs = timeUs;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2014 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.exoplayer.text.eia608;
|
||||
|
||||
/* package */ final class ClosedCaptionCtrl extends ClosedCaption {
|
||||
|
||||
/**
|
||||
* The receipt of the {@link #RESUME_CAPTION_LOADING} command initiates pop-on style captioning.
|
||||
* Subsequent data should be loaded into a non-displayed memory and held there until the
|
||||
* {@link #END_OF_CAPTION} command is received, at which point the non-displayed memory becomes
|
||||
* the displayed memory (and vice versa).
|
||||
*/
|
||||
public static final byte RESUME_CAPTION_LOADING = 0x20;
|
||||
/**
|
||||
* The receipt of the {@link #ROLL_UP_CAPTIONS_2_ROWS} command initiates roll-up style
|
||||
* captioning, with the maximum of 2 rows displayed simultaneously.
|
||||
*/
|
||||
public static final byte ROLL_UP_CAPTIONS_2_ROWS = 0x25;
|
||||
/**
|
||||
* The receipt of the {@link #ROLL_UP_CAPTIONS_3_ROWS} command initiates roll-up style
|
||||
* captioning, with the maximum of 3 rows displayed simultaneously.
|
||||
*/
|
||||
public static final byte ROLL_UP_CAPTIONS_3_ROWS = 0x26;
|
||||
/**
|
||||
* The receipt of the {@link #ROLL_UP_CAPTIONS_4_ROWS} command initiates roll-up style
|
||||
* captioning, with the maximum of 4 rows displayed simultaneously.
|
||||
*/
|
||||
public static final byte ROLL_UP_CAPTIONS_4_ROWS = 0x27;
|
||||
/**
|
||||
* The receipt of the {@link #RESUME_DIRECT_CAPTIONING} command initiates paint-on style
|
||||
* captioning. Subsequent data should be addressed immediately to displayed memory without need
|
||||
* for the {@link #RESUME_CAPTION_LOADING} command.
|
||||
*/
|
||||
public static final byte RESUME_DIRECT_CAPTIONING = 0x29;
|
||||
/**
|
||||
* The receipt of the {@link #END_OF_CAPTION} command indicates the end of pop-on style caption,
|
||||
* at this point already loaded in non-displayed memory caption should become the displayed
|
||||
* memory (and vice versa). If no {@link #RESUME_CAPTION_LOADING} command has been received,
|
||||
* {@link #END_OF_CAPTION} command forces the receiver into pop-on style.
|
||||
*/
|
||||
public static final byte END_OF_CAPTION = 0x2F;
|
||||
|
||||
public static final byte ERASE_DISPLAYED_MEMORY = 0x2C;
|
||||
public static final byte CARRIAGE_RETURN = 0x2D;
|
||||
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E;
|
||||
|
||||
|
||||
public static final byte MID_ROW_CHAN_1 = 0x11;
|
||||
public static final byte MID_ROW_CHAN_2 = 0x19;
|
||||
|
||||
public static final byte MISC_CHAN_1 = 0x14;
|
||||
public static final byte MISC_CHAN_2 = 0x1C;
|
||||
|
||||
public static final byte TAB_OFFSET_CHAN_1 = 0x17;
|
||||
public static final byte TAB_OFFSET_CHAN_2 = 0x1F;
|
||||
|
||||
public final byte cc1;
|
||||
public final byte cc2;
|
||||
|
||||
protected ClosedCaptionCtrl(byte cc1, byte cc2, long timeUs) {
|
||||
super(ClosedCaption.TYPE_CTRL, timeUs);
|
||||
this.cc1 = cc1;
|
||||
this.cc2 = cc2;
|
||||
}
|
||||
|
||||
public boolean isMidRowCode() {
|
||||
return (cc1 == MID_ROW_CHAN_1 || cc1 == MID_ROW_CHAN_2) && (cc2 >= 0x20 && cc2 <= 0x2F);
|
||||
}
|
||||
|
||||
public boolean isMiscCode() {
|
||||
return (cc1 == MISC_CHAN_1 || cc1 == MISC_CHAN_2) && (cc2 >= 0x20 && cc2 <= 0x2F);
|
||||
}
|
||||
|
||||
public boolean isTabOffsetCode() {
|
||||
return (cc1 == TAB_OFFSET_CHAN_1 || cc1 == TAB_OFFSET_CHAN_2) && (cc2 >= 0x21 && cc2 <= 0x23);
|
||||
}
|
||||
|
||||
public boolean isPreambleAddressCode() {
|
||||
return (cc1 >= 0x10 && cc1 <= 0x1F) && (cc2 >= 0x40 && cc2 <= 0x7F);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2014 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.exoplayer.text.eia608;
|
||||
|
||||
/* package */ final class ClosedCaptionText extends ClosedCaption {
|
||||
|
||||
public final String text;
|
||||
|
||||
public ClosedCaptionText(String text, long timeUs) {
|
||||
super(ClosedCaption.TYPE_TEXT, timeUs);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,8 +18,6 @@ package com.google.android.exoplayer.text.eia608;
|
|||
import com.google.android.exoplayer.util.BitArray;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -82,22 +80,29 @@ public class Eia608Parser {
|
|||
0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
|
||||
};
|
||||
|
||||
public boolean canParse(String mimeType) {
|
||||
private final BitArray seiBuffer;
|
||||
private final StringBuilder stringBuilder;
|
||||
|
||||
/* package */ Eia608Parser() {
|
||||
seiBuffer = new BitArray();
|
||||
stringBuilder = new StringBuilder();
|
||||
}
|
||||
|
||||
/* package */ boolean canParse(String mimeType) {
|
||||
return mimeType.equals(MimeTypes.APPLICATION_EIA608);
|
||||
}
|
||||
|
||||
public List<ClosedCaption> parse(byte[] data, int size, long timeUs) {
|
||||
/* package */ void parse(byte[] data, int size, long timeUs, List<ClosedCaption> out) {
|
||||
if (size <= 0) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
BitArray seiBuffer = new BitArray(data, size);
|
||||
|
||||
stringBuilder.setLength(0);
|
||||
seiBuffer.reset(data, size);
|
||||
seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit
|
||||
int ccCount = seiBuffer.readBits(5);
|
||||
seiBuffer.skipBytes(1);
|
||||
|
||||
List<ClosedCaption> captions = new ArrayList<ClosedCaption>();
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < ccCount; i++) {
|
||||
seiBuffer.skipBits(5); // one_bit + reserved
|
||||
boolean ccValid = seiBuffer.readBit();
|
||||
|
|
@ -129,12 +134,10 @@ public class Eia608Parser {
|
|||
// Control character.
|
||||
if (ccData1 < 0x20) {
|
||||
if (stringBuilder.length() > 0) {
|
||||
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(),
|
||||
timeUs));
|
||||
out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs));
|
||||
stringBuilder.setLength(0);
|
||||
}
|
||||
captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL,
|
||||
new String(new char[] {(char) ccData1, (char) ccData2}), timeUs));
|
||||
out.add(new ClosedCaptionCtrl(ccData1, ccData2, timeUs));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -146,10 +149,8 @@ public class Eia608Parser {
|
|||
}
|
||||
|
||||
if (stringBuilder.length() > 0) {
|
||||
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), timeUs));
|
||||
out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(captions);
|
||||
}
|
||||
|
||||
private static char getChar(byte ccData) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer.text.eia608;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.MediaFormatHolder;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
|
|
@ -22,6 +23,7 @@ import com.google.android.exoplayer.SampleSource;
|
|||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.text.TextRenderer;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Handler.Callback;
|
||||
|
|
@ -29,10 +31,8 @@ import android.os.Looper;
|
|||
import android.os.Message;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* A {@link TrackRenderer} for EIA-608 closed captions in a media stream.
|
||||
|
|
@ -40,26 +40,32 @@ import java.util.Queue;
|
|||
public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
||||
|
||||
private static final int MSG_INVOKE_RENDERER = 0;
|
||||
// The Number of closed captions text line to keep in memory.
|
||||
private static final int ALLOWED_CAPTIONS_TEXT_LINES_COUNT = 4;
|
||||
|
||||
private static final int CC_MODE_UNKNOWN = 0;
|
||||
private static final int CC_MODE_ROLL_UP = 1;
|
||||
private static final int CC_MODE_POP_ON = 2;
|
||||
private static final int CC_MODE_PAINT_ON = 3;
|
||||
|
||||
// The default number of rows to display in roll-up captions mode.
|
||||
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
|
||||
|
||||
private final SampleSource source;
|
||||
private final Eia608Parser eia608Parser;
|
||||
private final TextRenderer textRenderer;
|
||||
private final Handler metadataHandler;
|
||||
private final Handler textRendererHandler;
|
||||
private final MediaFormatHolder formatHolder;
|
||||
private final SampleHolder sampleHolder;
|
||||
private final StringBuilder closedCaptionStringBuilder;
|
||||
//Currently displayed captions.
|
||||
private final List<ClosedCaption> currentCaptions;
|
||||
private final Queue<Integer> newLineIndexes;
|
||||
private final StringBuilder captionStringBuilder;
|
||||
private final List<ClosedCaption> captionBuffer;
|
||||
|
||||
private int trackIndex;
|
||||
private long currentPositionUs;
|
||||
private boolean inputStreamEnded;
|
||||
|
||||
private long pendingCaptionsTimestamp;
|
||||
private List<ClosedCaption> pendingCaptions;
|
||||
private int captionMode;
|
||||
private int captionRowCount;
|
||||
private String caption;
|
||||
private String lastRenderedCaption;
|
||||
|
||||
/**
|
||||
* @param source A source from which samples containing EIA-608 closed captions can be read.
|
||||
|
|
@ -74,14 +80,12 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
Looper textRendererLooper) {
|
||||
this.source = Assertions.checkNotNull(source);
|
||||
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
||||
this.metadataHandler = textRendererLooper == null ? null
|
||||
: new Handler(textRendererLooper, this);
|
||||
textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this);
|
||||
eia608Parser = new Eia608Parser();
|
||||
formatHolder = new MediaFormatHolder();
|
||||
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
closedCaptionStringBuilder = new StringBuilder();
|
||||
currentCaptions = new LinkedList<ClosedCaption>();
|
||||
newLineIndexes = new LinkedList<Integer>();
|
||||
captionStringBuilder = new StringBuilder();
|
||||
captionBuffer = new ArrayList<ClosedCaption>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -117,10 +121,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
|
||||
private void seekToInternal(long positionUs) {
|
||||
currentPositionUs = positionUs;
|
||||
pendingCaptions = null;
|
||||
inputStreamEnded = false;
|
||||
// Clear displayed captions.
|
||||
currentCaptions.clear();
|
||||
clearPendingSample();
|
||||
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
|
||||
setCaptionMode(CC_MODE_UNKNOWN);
|
||||
invokeRenderer(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -133,15 +138,10 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
|
||||
if (!inputStreamEnded && pendingCaptions == null) {
|
||||
if (!inputStreamEnded && !isSamplePending()) {
|
||||
try {
|
||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
pendingCaptionsTimestamp = sampleHolder.timeUs;
|
||||
pendingCaptions = eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size,
|
||||
sampleHolder.timeUs);
|
||||
sampleHolder.data.clear();
|
||||
} else if (result == SampleSource.END_OF_STREAM) {
|
||||
if (result == SampleSource.END_OF_STREAM) {
|
||||
inputStreamEnded = true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
|
@ -149,15 +149,22 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
}
|
||||
}
|
||||
|
||||
if (pendingCaptions != null && pendingCaptionsTimestamp <= currentPositionUs) {
|
||||
invokeRenderer(pendingCaptions);
|
||||
pendingCaptions = null;
|
||||
if (isSamplePending() && sampleHolder.timeUs <= currentPositionUs) {
|
||||
// Parse the pending sample.
|
||||
eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size, sampleHolder.timeUs,
|
||||
captionBuffer);
|
||||
// Consume parsed captions.
|
||||
consumeCaptionBuffer();
|
||||
// Update the renderer, unless the sample was marked for decoding only.
|
||||
if (!sampleHolder.decodeOnly) {
|
||||
invokeRenderer(caption);
|
||||
}
|
||||
clearPendingSample();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
pendingCaptions = null;
|
||||
source.disable(trackIndex);
|
||||
}
|
||||
|
||||
|
|
@ -186,11 +193,16 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
return true;
|
||||
}
|
||||
|
||||
private void invokeRenderer(List<ClosedCaption> metadata) {
|
||||
if (metadataHandler != null) {
|
||||
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
|
||||
private void invokeRenderer(String text) {
|
||||
if (Util.areEqual(lastRenderedCaption, text)) {
|
||||
// No change.
|
||||
return;
|
||||
}
|
||||
this.lastRenderedCaption = text;
|
||||
if (textRendererHandler != null) {
|
||||
textRendererHandler.obtainMessage(MSG_INVOKE_RENDERER, text).sendToTarget();
|
||||
} else {
|
||||
invokeRendererInternal(metadata);
|
||||
invokeRendererInternal(text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,62 +211,155 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
public boolean handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_INVOKE_RENDERER:
|
||||
invokeRendererInternal((List<ClosedCaption>) msg.obj);
|
||||
invokeRendererInternal((String) msg.obj);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void invokeRendererInternal(List<ClosedCaption> metadata) {
|
||||
currentCaptions.addAll(metadata);
|
||||
// Sort captions by the timestamp.
|
||||
Collections.sort(currentCaptions);
|
||||
closedCaptionStringBuilder.setLength(0);
|
||||
private void invokeRendererInternal(String text) {
|
||||
textRenderer.onText(text);
|
||||
}
|
||||
|
||||
// After processing keep only captions after cutIndex.
|
||||
int cutIndex = 0;
|
||||
newLineIndexes.clear();
|
||||
for (int i = 0; i < currentCaptions.size(); i++) {
|
||||
ClosedCaption caption = currentCaptions.get(i);
|
||||
private void consumeCaptionBuffer() {
|
||||
int captionBufferSize = captionBuffer.size();
|
||||
if (captionBufferSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < captionBufferSize; i++) {
|
||||
ClosedCaption caption = captionBuffer.get(i);
|
||||
if (caption.type == ClosedCaption.TYPE_CTRL) {
|
||||
int cc2 = caption.text.codePointAt(1);
|
||||
switch (cc2) {
|
||||
case 0x2C: // Erase Displayed Memory.
|
||||
closedCaptionStringBuilder.setLength(0);
|
||||
cutIndex = i;
|
||||
newLineIndexes.clear();
|
||||
break;
|
||||
case 0x25: // Roll-Up.
|
||||
case 0x26:
|
||||
case 0x27:
|
||||
default:
|
||||
if (cc2 >= 0x20 && cc2 < 0x40) {
|
||||
break;
|
||||
}
|
||||
if (closedCaptionStringBuilder.length() > 0
|
||||
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1)
|
||||
!= '\n') {
|
||||
closedCaptionStringBuilder.append('\n');
|
||||
newLineIndexes.add(i);
|
||||
if (newLineIndexes.size() >= ALLOWED_CAPTIONS_TEXT_LINES_COUNT) {
|
||||
cutIndex = newLineIndexes.poll();
|
||||
}
|
||||
}
|
||||
break;
|
||||
ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption;
|
||||
if (captionCtrl.isMiscCode()) {
|
||||
handleMiscCode(captionCtrl);
|
||||
} else if (captionCtrl.isPreambleAddressCode()) {
|
||||
handlePreambleAddressCode();
|
||||
}
|
||||
} else {
|
||||
closedCaptionStringBuilder.append(caption.text);
|
||||
handleText((ClosedCaptionText) caption);
|
||||
}
|
||||
}
|
||||
captionBuffer.clear();
|
||||
|
||||
if (cutIndex > 0 && cutIndex < currentCaptions.size() - 1) {
|
||||
for (int i = 0; i <= cutIndex; i++) {
|
||||
currentCaptions.remove(0);
|
||||
}
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
|
||||
caption = getDisplayCaption();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleText(ClosedCaptionText captionText) {
|
||||
if (captionMode != CC_MODE_UNKNOWN) {
|
||||
captionStringBuilder.append(captionText.text);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMiscCode(ClosedCaptionCtrl captionCtrl) {
|
||||
switch (captionCtrl.cc2) {
|
||||
case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_2_ROWS:
|
||||
captionRowCount = 2;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
return;
|
||||
case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_3_ROWS:
|
||||
captionRowCount = 3;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
return;
|
||||
case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_4_ROWS:
|
||||
captionRowCount = 4;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
return;
|
||||
case ClosedCaptionCtrl.RESUME_CAPTION_LOADING:
|
||||
setCaptionMode(CC_MODE_POP_ON);
|
||||
return;
|
||||
case ClosedCaptionCtrl.RESUME_DIRECT_CAPTIONING:
|
||||
setCaptionMode(CC_MODE_PAINT_ON);
|
||||
return;
|
||||
}
|
||||
|
||||
textRenderer.onText(closedCaptionStringBuilder.toString());
|
||||
if (captionMode == CC_MODE_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (captionCtrl.cc2) {
|
||||
case ClosedCaptionCtrl.ERASE_DISPLAYED_MEMORY:
|
||||
caption = null;
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
|
||||
captionStringBuilder.setLength(0);
|
||||
}
|
||||
return;
|
||||
case ClosedCaptionCtrl.ERASE_NON_DISPLAYED_MEMORY:
|
||||
captionStringBuilder.setLength(0);
|
||||
return;
|
||||
case ClosedCaptionCtrl.END_OF_CAPTION:
|
||||
caption = getDisplayCaption();
|
||||
captionStringBuilder.setLength(0);
|
||||
return;
|
||||
case ClosedCaptionCtrl.CARRIAGE_RETURN:
|
||||
maybeAppendNewline();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePreambleAddressCode() {
|
||||
// TODO: Add better handling of this with specific positioning.
|
||||
maybeAppendNewline();
|
||||
}
|
||||
|
||||
private void setCaptionMode(int captionMode) {
|
||||
if (this.captionMode == captionMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.captionMode = captionMode;
|
||||
// Clear the working memory.
|
||||
captionStringBuilder.setLength(0);
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) {
|
||||
// When switching to roll-up or unknown, we also need to clear the caption.
|
||||
caption = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeAppendNewline() {
|
||||
int buildLength = captionStringBuilder.length();
|
||||
if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') {
|
||||
captionStringBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
private String getDisplayCaption() {
|
||||
int buildLength = captionStringBuilder.length();
|
||||
if (buildLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean endsWithNewline = captionStringBuilder.charAt(buildLength - 1) == '\n';
|
||||
if (buildLength == 1 && endsWithNewline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int endIndex = endsWithNewline ? buildLength - 1 : buildLength;
|
||||
if (captionMode != CC_MODE_ROLL_UP) {
|
||||
return captionStringBuilder.substring(0, endIndex);
|
||||
}
|
||||
|
||||
int startIndex = 0;
|
||||
int searchBackwardFromIndex = endIndex;
|
||||
for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) {
|
||||
searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1);
|
||||
}
|
||||
if (searchBackwardFromIndex != -1) {
|
||||
startIndex = searchBackwardFromIndex + 1;
|
||||
}
|
||||
captionStringBuilder.delete(0, startIndex);
|
||||
return captionStringBuilder.substring(0, endIndex - startIndex);
|
||||
}
|
||||
|
||||
private void clearPendingSample() {
|
||||
sampleHolder.timeUs = C.UNKNOWN_TIME_US;
|
||||
sampleHolder.clearData();
|
||||
}
|
||||
|
||||
private boolean isSamplePending() {
|
||||
return sampleHolder.timeUs != C.UNKNOWN_TIME_US;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,6 +401,32 @@ public final class Util {
|
|||
return scaledTimestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps.
|
||||
*
|
||||
* @param timestamps The timestamps to scale.
|
||||
* @param multiplier The multiplier.
|
||||
* @param divisor The divisor.
|
||||
*/
|
||||
public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {
|
||||
if (divisor >= multiplier && (divisor % multiplier) == 0) {
|
||||
long divisionFactor = divisor / multiplier;
|
||||
for (int i = 0; i < timestamps.length; i++) {
|
||||
timestamps[i] /= divisionFactor;
|
||||
}
|
||||
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
|
||||
long multiplicationFactor = multiplier / divisor;
|
||||
for (int i = 0; i < timestamps.length; i++) {
|
||||
timestamps[i] *= multiplicationFactor;
|
||||
}
|
||||
} else {
|
||||
double multiplicationFactor = (double) multiplier / divisor;
|
||||
for (int i = 0; i < timestamps.length; i++) {
|
||||
timestamps[i] = (long) (timestamps[i] * multiplicationFactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of integers to a primitive array.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue