Commit b0a3c30a by Oliver Woodman

Improve EIA608 caption support.

- Also make text renderers respect the decodeOnly flag.
- Also fix AC3 passthrough to always allocate direct buffers.
parent 32f0eb12
...@@ -25,7 +25,6 @@ import android.media.AudioFormat; ...@@ -25,7 +25,6 @@ import android.media.AudioFormat;
import android.os.Handler; import android.os.Handler;
import java.io.IOException; 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 * 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 { ...@@ -105,8 +104,8 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
this.source = Assertions.checkNotNull(source); this.source = Assertions.checkNotNull(source);
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.eventListener = eventListener; this.eventListener = eventListener;
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
sampleHolder.data = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE); sampleHolder.replaceBuffer(DEFAULT_BUFFER_SIZE);
formatHolder = new MediaFormatHolder(); formatHolder = new MediaFormatHolder();
audioTrack = new AudioTrack(); audioTrack = new AudioTrack();
shouldReadInputBuffer = true; shouldReadInputBuffer = true;
...@@ -199,8 +198,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer { ...@@ -199,8 +198,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
// Get more data if we have run out. // Get more data if we have run out.
if (shouldReadInputBuffer) { if (shouldReadInputBuffer) {
sampleHolder.data.clear(); sampleHolder.clearData();
int result = int result =
source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.FORMAT_READ) { if (result == SampleSource.FORMAT_READ) {
......
...@@ -96,4 +96,13 @@ public final class SampleHolder { ...@@ -96,4 +96,13 @@ public final class SampleHolder {
return false; return false;
} }
/**
* Clears {@link #data}. Does nothing if {@link #data} is null.
*/
public void clearData() {
if (data != null) {
data.clear();
}
}
} }
...@@ -34,7 +34,9 @@ import android.util.SparseArray; ...@@ -34,7 +34,9 @@ import android.util.SparseArray;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
/** /**
...@@ -541,7 +543,6 @@ public final class TsExtractor { ...@@ -541,7 +543,6 @@ public final class TsExtractor {
@SuppressWarnings("hiding") @SuppressWarnings("hiding")
private final SamplePool samplePool; private final SamplePool samplePool;
private final ConcurrentLinkedQueue<Sample> internalQueue;
// Accessed only by the consuming thread. // Accessed only by the consuming thread.
private boolean needKeyframe; private boolean needKeyframe;
...@@ -553,7 +554,6 @@ public final class TsExtractor { ...@@ -553,7 +554,6 @@ public final class TsExtractor {
protected SampleQueue(SamplePool samplePool) { protected SampleQueue(SamplePool samplePool) {
this.samplePool = samplePool; this.samplePool = samplePool;
internalQueue = new ConcurrentLinkedQueue<Sample>();
needKeyframe = true; needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE; lastReadTimeUs = Long.MIN_VALUE;
spliceOutTimeUs = Long.MIN_VALUE; spliceOutTimeUs = Long.MIN_VALUE;
...@@ -582,7 +582,7 @@ public final class TsExtractor { ...@@ -582,7 +582,7 @@ public final class TsExtractor {
public Sample poll() { public Sample poll() {
Sample head = peek(); Sample head = peek();
if (head != null) { if (head != null) {
internalQueue.remove(); internalPollSample();
needKeyframe = false; needKeyframe = false;
lastReadTimeUs = head.timeUs; lastReadTimeUs = head.timeUs;
} }
...@@ -595,13 +595,13 @@ public final class TsExtractor { ...@@ -595,13 +595,13 @@ public final class TsExtractor {
* @return The next sample from the queue, or null if a sample isn't available. * @return The next sample from the queue, or null if a sample isn't available.
*/ */
public Sample peek() { public Sample peek() {
Sample head = internalQueue.peek(); Sample head = internalPeekSample();
if (needKeyframe) { if (needKeyframe) {
// Peeking discard of samples until we find a keyframe or run out of available samples. // Peeking discard of samples until we find a keyframe or run out of available samples.
while (head != null && !head.isKeyframe) { while (head != null && !head.isKeyframe) {
recycle(head); recycle(head);
internalQueue.remove(); internalPollSample();
head = internalQueue.peek(); head = internalPeekSample();
} }
} }
if (head == null) { if (head == null) {
...@@ -610,7 +610,7 @@ public final class TsExtractor { ...@@ -610,7 +610,7 @@ public final class TsExtractor {
if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) { if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) {
// The sample is later than the time this queue is spliced out. // The sample is later than the time this queue is spliced out.
recycle(head); recycle(head);
internalQueue.remove(); internalPollSample();
return null; return null;
} }
return head; return head;
...@@ -625,8 +625,8 @@ public final class TsExtractor { ...@@ -625,8 +625,8 @@ public final class TsExtractor {
Sample head = peek(); Sample head = peek();
while (head != null && head.timeUs < timeUs) { while (head != null && head.timeUs < timeUs) {
recycle(head); recycle(head);
internalQueue.remove(); internalPollSample();
head = internalQueue.peek(); head = internalPeekSample();
// We're discarding at least one sample, so any subsequent read will need to start at // We're discarding at least one sample, so any subsequent read will need to start at
// a keyframe. // a keyframe.
needKeyframe = true; needKeyframe = true;
...@@ -638,10 +638,10 @@ public final class TsExtractor { ...@@ -638,10 +638,10 @@ public final class TsExtractor {
* Clears the queue. * Clears the queue.
*/ */
public void release() { public void release() {
Sample toRecycle = internalQueue.poll(); Sample toRecycle = internalPollSample();
while (toRecycle != null) { while (toRecycle != null) {
recycle(toRecycle); recycle(toRecycle);
toRecycle = internalQueue.poll(); toRecycle = internalPollSample();
} }
} }
...@@ -666,20 +666,19 @@ public final class TsExtractor { ...@@ -666,20 +666,19 @@ public final class TsExtractor {
return true; return true;
} }
long firstPossibleSpliceTime; long firstPossibleSpliceTime;
Sample nextSample = internalQueue.peek(); Sample nextSample = internalPeekSample();
if (nextSample != null) { if (nextSample != null) {
firstPossibleSpliceTime = nextSample.timeUs; firstPossibleSpliceTime = nextSample.timeUs;
} else { } else {
firstPossibleSpliceTime = lastReadTimeUs + 1; firstPossibleSpliceTime = lastReadTimeUs + 1;
} }
ConcurrentLinkedQueue<Sample> nextInternalQueue = nextQueue.internalQueue; Sample nextQueueSample = nextQueue.internalPeekSample();
Sample nextQueueSample = nextInternalQueue.peek();
while (nextQueueSample != null while (nextQueueSample != null
&& (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { && (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) {
// Discard samples from the next queue for as long as they are before the earliest possible // Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes. // splice time, or not keyframes.
nextQueue.internalQueue.remove(); nextQueue.internalPollSample();
nextQueueSample = nextQueue.internalQueue.peek(); nextQueueSample = nextQueue.internalPeekSample();
} }
if (nextQueueSample != null) { if (nextQueueSample != null) {
// We've found a keyframe in the next queue that can serve as the splice point. Set the // 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 { ...@@ -720,7 +719,7 @@ public final class TsExtractor {
protected void addSample(Sample sample) { protected void addSample(Sample sample) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
internalQueue.add(sample); internalQueueSample(sample);
} }
protected void addToSample(Sample sample, BitArray buffer, int size) { protected void addToSample(Sample sample, BitArray buffer, int size) {
...@@ -731,15 +730,37 @@ public final class TsExtractor { ...@@ -731,15 +730,37 @@ public final class TsExtractor {
sample.size += size; 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 abstract class PesPayloadReader extends SampleQueue {
private final ConcurrentLinkedQueue<Sample> internalQueue;
protected PesPayloadReader(SamplePool samplePool) { protected PesPayloadReader(SamplePool samplePool) {
super(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); public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs);
...@@ -992,18 +1013,23 @@ public final class TsExtractor { ...@@ -992,18 +1013,23 @@ public final class TsExtractor {
/** /**
* Parses a SEI data from H.264 frames and extracts samples with closed captions data. * Parses a SEI data from H.264 frames and extracts samples with closed captions data.
*
* 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. // SEI data, used for Closed Captions.
private static final int NAL_UNIT_TYPE_SEI = 6; private static final int NAL_UNIT_TYPE_SEI = 6;
private final BitArray seiBuffer; private final BitArray seiBuffer;
private final TreeSet<Sample> internalQueue;
public SeiReader(SamplePool samplePool) { public SeiReader(SamplePool samplePool) {
super(samplePool); super(samplePool);
setMediaFormat(MediaFormat.createEia608Format()); setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new BitArray(); seiBuffer = new BitArray();
internalQueue = new TreeSet<Sample>(this);
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
...@@ -1022,6 +1048,27 @@ public final class TsExtractor { ...@@ -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);
}
} }
/** /**
......
...@@ -135,7 +135,6 @@ public class SubtitleParserHelper implements Handler.Callback { ...@@ -135,7 +135,6 @@ public class SubtitleParserHelper implements Handler.Callback {
if (sampleHolder != holder) { if (sampleHolder != holder) {
// A flush has occurred since this holder was posted. Do nothing. // A flush has occurred since this holder was posted. Do nothing.
} else { } else {
holder.data.position(0);
this.result = result; this.result = result;
this.error = error; this.error = error;
this.parsing = false; this.parsing = false;
......
...@@ -177,8 +177,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { ...@@ -177,8 +177,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
if (!inputStreamEnded && subtitle == null) { if (!inputStreamEnded && subtitle == null) {
try { try {
SampleHolder sampleHolder = parserHelper.getSampleHolder(); SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData();
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) { if (result == SampleSource.SAMPLE_READ && !sampleHolder.decodeOnly) {
parserHelper.startParseOperation(); parserHelper.startParseOperation();
textRendererNeedsUpdate = false; textRendererNeedsUpdate = false;
} else if (result == SampleSource.END_OF_STREAM) { } else if (result == SampleSource.END_OF_STREAM) {
......
...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608; ...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608;
/** /**
* A Closed Caption that contains textual data associated with time indices. * 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. * Identifies closed captions with control characters.
...@@ -30,23 +30,16 @@ public final class ClosedCaption implements Comparable<ClosedCaption> { ...@@ -30,23 +30,16 @@ public final class ClosedCaption implements Comparable<ClosedCaption> {
public static final int TYPE_TEXT = 1; public static final int TYPE_TEXT = 1;
/** /**
* The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field * The type of the closed caption data.
* has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control
* characters (C1, C2).
*/ */
public final int type; public final int type;
/** /**
* Contains text or two control characters.
*/
public final String text;
/**
* Timestamp associated with the closed caption. * Timestamp associated with the closed caption.
*/ */
public final long timeUs; public final long timeUs;
public ClosedCaption(int type, String text, long timeUs) { protected ClosedCaption(int type, long timeUs) {
this.type = type; this.type = type;
this.text = text;
this.timeUs = timeUs; this.timeUs = timeUs;
} }
......
/*
* 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);
}
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text.eia608;
/* package */ final class ClosedCaptionText extends ClosedCaption {
public final String text;
public ClosedCaptionText(String text, long timeUs) {
super(ClosedCaption.TYPE_TEXT, timeUs);
this.text = text;
}
}
...@@ -18,8 +18,6 @@ package com.google.android.exoplayer.text.eia608; ...@@ -18,8 +18,6 @@ package com.google.android.exoplayer.text.eia608;
import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
...@@ -82,22 +80,29 @@ public class Eia608Parser { ...@@ -82,22 +80,29 @@ public class Eia608Parser {
0xFB // 3F: 251 'û' "Latin small letter U with circumflex" 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); 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) { 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 seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit
int ccCount = seiBuffer.readBits(5); int ccCount = seiBuffer.readBits(5);
seiBuffer.skipBytes(1); seiBuffer.skipBytes(1);
List<ClosedCaption> captions = new ArrayList<ClosedCaption>();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < ccCount; i++) { for (int i = 0; i < ccCount; i++) {
seiBuffer.skipBits(5); // one_bit + reserved seiBuffer.skipBits(5); // one_bit + reserved
boolean ccValid = seiBuffer.readBit(); boolean ccValid = seiBuffer.readBit();
...@@ -129,12 +134,10 @@ public class Eia608Parser { ...@@ -129,12 +134,10 @@ public class Eia608Parser {
// Control character. // Control character.
if (ccData1 < 0x20) { if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) { if (stringBuilder.length() > 0) {
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs));
timeUs));
stringBuilder.setLength(0); stringBuilder.setLength(0);
} }
captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL, out.add(new ClosedCaptionCtrl(ccData1, ccData2, timeUs));
new String(new char[] {(char) ccData1, (char) ccData2}), timeUs));
continue; continue;
} }
...@@ -146,10 +149,8 @@ public class Eia608Parser { ...@@ -146,10 +149,8 @@ public class Eia608Parser {
} }
if (stringBuilder.length() > 0) { 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) { private static char getChar(byte ccData) {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.text.eia608; package com.google.android.exoplayer.text.eia608;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
...@@ -22,6 +23,7 @@ import com.google.android.exoplayer.SampleSource; ...@@ -22,6 +23,7 @@ import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.text.TextRenderer; import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.os.Handler; import android.os.Handler;
import android.os.Handler.Callback; import android.os.Handler.Callback;
...@@ -29,10 +31,8 @@ import android.os.Looper; ...@@ -29,10 +31,8 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue;
/** /**
* A {@link TrackRenderer} for EIA-608 closed captions in a media stream. * A {@link TrackRenderer} for EIA-608 closed captions in a media stream.
...@@ -40,26 +40,32 @@ import java.util.Queue; ...@@ -40,26 +40,32 @@ import java.util.Queue;
public class Eia608TrackRenderer extends TrackRenderer implements Callback { public class Eia608TrackRenderer extends TrackRenderer implements Callback {
private static final int MSG_INVOKE_RENDERER = 0; 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 SampleSource source;
private final Eia608Parser eia608Parser; private final Eia608Parser eia608Parser;
private final TextRenderer textRenderer; private final TextRenderer textRenderer;
private final Handler metadataHandler; private final Handler textRendererHandler;
private final MediaFormatHolder formatHolder; private final MediaFormatHolder formatHolder;
private final SampleHolder sampleHolder; private final SampleHolder sampleHolder;
private final StringBuilder closedCaptionStringBuilder; private final StringBuilder captionStringBuilder;
//Currently displayed captions. private final List<ClosedCaption> captionBuffer;
private final List<ClosedCaption> currentCaptions;
private final Queue<Integer> newLineIndexes;
private int trackIndex; private int trackIndex;
private long currentPositionUs; private long currentPositionUs;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private long pendingCaptionsTimestamp; private int captionMode;
private List<ClosedCaption> pendingCaptions; private int captionRowCount;
private String caption;
private String lastRenderedCaption;
/** /**
* @param source A source from which samples containing EIA-608 closed captions can be read. * @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 { ...@@ -74,14 +80,12 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
Looper textRendererLooper) { Looper textRendererLooper) {
this.source = Assertions.checkNotNull(source); this.source = Assertions.checkNotNull(source);
this.textRenderer = Assertions.checkNotNull(textRenderer); this.textRenderer = Assertions.checkNotNull(textRenderer);
this.metadataHandler = textRendererLooper == null ? null textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this);
: new Handler(textRendererLooper, this);
eia608Parser = new Eia608Parser(); eia608Parser = new Eia608Parser();
formatHolder = new MediaFormatHolder(); formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
closedCaptionStringBuilder = new StringBuilder(); captionStringBuilder = new StringBuilder();
currentCaptions = new LinkedList<ClosedCaption>(); captionBuffer = new ArrayList<ClosedCaption>();
newLineIndexes = new LinkedList<Integer>();
} }
@Override @Override
...@@ -117,10 +121,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -117,10 +121,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
private void seekToInternal(long positionUs) { private void seekToInternal(long positionUs) {
currentPositionUs = positionUs; currentPositionUs = positionUs;
pendingCaptions = null;
inputStreamEnded = false; inputStreamEnded = false;
// Clear displayed captions. clearPendingSample();
currentCaptions.clear(); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
setCaptionMode(CC_MODE_UNKNOWN);
invokeRenderer(null);
} }
@Override @Override
...@@ -133,15 +138,10 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -133,15 +138,10 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
throw new ExoPlaybackException(e); throw new ExoPlaybackException(e);
} }
if (!inputStreamEnded && pendingCaptions == null) { if (!inputStreamEnded && !isSamplePending()) {
try { try {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) { if (result == SampleSource.END_OF_STREAM) {
pendingCaptionsTimestamp = sampleHolder.timeUs;
pendingCaptions = eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size,
sampleHolder.timeUs);
sampleHolder.data.clear();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true; inputStreamEnded = true;
} }
} catch (IOException e) { } catch (IOException e) {
...@@ -149,15 +149,22 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -149,15 +149,22 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
} }
} }
if (pendingCaptions != null && pendingCaptionsTimestamp <= currentPositionUs) { if (isSamplePending() && sampleHolder.timeUs <= currentPositionUs) {
invokeRenderer(pendingCaptions); // Parse the pending sample.
pendingCaptions = null; 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 @Override
protected void onDisabled() { protected void onDisabled() {
pendingCaptions = null;
source.disable(trackIndex); source.disable(trackIndex);
} }
...@@ -186,11 +193,16 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -186,11 +193,16 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
return true; return true;
} }
private void invokeRenderer(List<ClosedCaption> metadata) { private void invokeRenderer(String text) {
if (metadataHandler != null) { if (Util.areEqual(lastRenderedCaption, text)) {
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); // No change.
return;
}
this.lastRenderedCaption = text;
if (textRendererHandler != null) {
textRendererHandler.obtainMessage(MSG_INVOKE_RENDERER, text).sendToTarget();
} else { } else {
invokeRendererInternal(metadata); invokeRendererInternal(text);
} }
} }
...@@ -199,62 +211,155 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -199,62 +211,155 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case MSG_INVOKE_RENDERER: case MSG_INVOKE_RENDERER:
invokeRendererInternal((List<ClosedCaption>) msg.obj); invokeRendererInternal((String) msg.obj);
return true; return true;
} }
return false; return false;
} }
private void invokeRendererInternal(List<ClosedCaption> metadata) { private void invokeRendererInternal(String text) {
currentCaptions.addAll(metadata); textRenderer.onText(text);
// Sort captions by the timestamp. }
Collections.sort(currentCaptions);
closedCaptionStringBuilder.setLength(0);
// After processing keep only captions after cutIndex. private void consumeCaptionBuffer() {
int cutIndex = 0; int captionBufferSize = captionBuffer.size();
newLineIndexes.clear(); if (captionBufferSize == 0) {
for (int i = 0; i < currentCaptions.size(); i++) { return;
ClosedCaption caption = currentCaptions.get(i); }
for (int i = 0; i < captionBufferSize; i++) {
ClosedCaption caption = captionBuffer.get(i);
if (caption.type == ClosedCaption.TYPE_CTRL) { if (caption.type == ClosedCaption.TYPE_CTRL) {
int cc2 = caption.text.codePointAt(1); ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption;
switch (cc2) { if (captionCtrl.isMiscCode()) {
case 0x2C: // Erase Displayed Memory. handleMiscCode(captionCtrl);
closedCaptionStringBuilder.setLength(0); } else if (captionCtrl.isPreambleAddressCode()) {
cutIndex = i; handlePreambleAddressCode();
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;
} }
} else { } else {
closedCaptionStringBuilder.append(caption.text); handleText((ClosedCaptionText) caption);
} }
} }
captionBuffer.clear();
if (cutIndex > 0 && cutIndex < currentCaptions.size() - 1) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
for (int i = 0; i <= cutIndex; i++) { caption = getDisplayCaption();
currentCaptions.remove(0);
}
} }
}
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;
}
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();
}
textRenderer.onText(closedCaptionStringBuilder.toString()); private boolean isSamplePending() {
return sampleHolder.timeUs != C.UNKNOWN_TIME_US;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment