From 32f0eb1278dacc1c78faf031a4a1db86ab617bac Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 9 Feb 2015 17:25:39 +0000 Subject: [PATCH 1/2] Enhance mp4 parsing. --- .../parser/mp4/FragmentedMp4Extractor.java | 4 +- .../google/android/exoplayer/mp4/Atom.java | 2 + .../exoplayer/mp4/CommonMp4AtomParsers.java | 203 +++++++++++++++++- .../exoplayer/mp4/Mp4TrackSampleTable.java | 88 ++++++++ .../google/android/exoplayer/mp4/Mp4Util.java | 9 + .../exoplayer/source/SampleExtractor.java | 3 +- .../google/android/exoplayer/util/Util.java | 26 +++ 7 files changed, 328 insertions(+), 7 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java index 01646ea66f..274a4664aa 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java @@ -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) { diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java index 3451d31929..292c231087 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java @@ -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"); diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java index dc7e580ca5..22e5b1edcb 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java @@ -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 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 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). * diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java new file mode 100644 index 0000000000..325247edf1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java @@ -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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index 471689e24a..a78c9fb414 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -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}; diff --git a/library/src/main/java/com/google/android/exoplayer/source/SampleExtractor.java b/library/src/main/java/com/google/android/exoplayer/source/SampleExtractor.java index bce39cc9fc..383aa80ce2 100644 --- a/library/src/main/java/com/google/android/exoplayer/source/SampleExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/source/SampleExtractor.java @@ -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(); diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index ae2e014f0d..7e096cfa1b 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -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. * From b0a3c30a90039bad8d2364c6d214836d5d437c81 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 9 Feb 2015 17:28:07 +0000 Subject: [PATCH 2/2] Improve EIA608 caption support. - Also make text renderers respect the decodeOnly flag. - Also fix AC3 passthrough to always allocate direct buffers. --- .../Ac3PassthroughAudioTrackRenderer.java | 8 +- .../android/exoplayer/SampleHolder.java | 9 + .../android/exoplayer/hls/TsExtractor.java | 85 ++++-- .../exoplayer/text/SubtitleParserHelper.java | 1 - .../exoplayer/text/TextTrackRenderer.java | 3 +- .../exoplayer/text/eia608/ClosedCaption.java | 13 +- .../text/eia608/ClosedCaptionCtrl.java | 95 +++++++ .../text/eia608/ClosedCaptionText.java | 27 ++ .../exoplayer/text/eia608/Eia608Parser.java | 33 +-- .../text/eia608/Eia608TrackRenderer.java | 257 ++++++++++++------ 10 files changed, 403 insertions(+), 128 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java create mode 100644 library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java diff --git a/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java index 549b9dcd55..44325b44ea 100644 --- a/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java @@ -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) { diff --git a/library/src/main/java/com/google/android/exoplayer/SampleHolder.java b/library/src/main/java/com/google/android/exoplayer/SampleHolder.java index 43308bc40b..9a258b752c 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleHolder.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleHolder.java @@ -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(); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 1c9dcc8804..5452391e24 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -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 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(); 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 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 internalQueue; + protected PesPayloadReader(SamplePool samplePool) { super(samplePool); + internalQueue = new ConcurrentLinkedQueue(); + } + + @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 { // SEI data, used for Closed Captions. private static final int NAL_UNIT_TYPE_SEI = 6; private final BitArray seiBuffer; + private final TreeSet internalQueue; public SeiReader(SamplePool samplePool) { super(samplePool); setMediaFormat(MediaFormat.createEia608Format()); seiBuffer = new BitArray(); + internalQueue = new TreeSet(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); + } + } /** diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java index 38958aa0b3..69872bf385 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParserHelper.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 7f3dba9654..e2452102d1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -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) { diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java index dad39fc359..ab6aff54c6 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java @@ -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 { +/* package */ abstract class ClosedCaption implements Comparable { /** * Identifies closed captions with control characters. @@ -30,23 +30,16 @@ public final class ClosedCaption implements Comparable { 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; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java new file mode 100644 index 0000000000..ceca05c919 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java @@ -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); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java new file mode 100644 index 0000000000..49fbc5af2d --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java @@ -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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index 94fa56ccb3..8d4b0f5b06 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -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 parse(byte[] data, int size, long timeUs) { + /* package */ void parse(byte[] data, int size, long timeUs, List 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 captions = new ArrayList(); - - 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) { diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index d88e44d506..349c1450b8 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -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 currentCaptions; - private final Queue newLineIndexes; + private final StringBuilder captionStringBuilder; + private final List captionBuffer; private int trackIndex; private long currentPositionUs; private boolean inputStreamEnded; - private long pendingCaptionsTimestamp; - private List 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(); - newLineIndexes = new LinkedList(); + captionStringBuilder = new StringBuilder(); + captionBuffer = new ArrayList(); } @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 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) msg.obj); + invokeRendererInternal((String) msg.obj); return true; } return false; } - private void invokeRendererInternal(List 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; } }