Merge pull request #287 from google/dev

dev -> dev-webm-vp9-opus
This commit is contained in:
ojw28 2015-02-09 17:29:44 +00:00
commit ccac9fad4e
17 changed files with 731 additions and 135 deletions

View file

@ -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) {

View file

@ -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();
}
}
}

View file

@ -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) {

View file

@ -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);
}
}
/**

View file

@ -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");

View file

@ -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).
*

View file

@ -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;
}
}

View file

@ -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};

View file

@ -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();

View file

@ -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;

View file

@ -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) {

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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.
*