mirror of
https://github.com/samsonjs/media.git
synced 2026-04-12 12:25:47 +00:00
Improve EIA608 caption support.
- Also make text renderers respect the decodeOnly flag. - Also fix AC3 passthrough to always allocate direct buffers.
This commit is contained in:
parent
32f0eb1278
commit
b0a3c30a90
10 changed files with 403 additions and 128 deletions
|
|
@ -25,7 +25,6 @@ import android.media.AudioFormat;
|
|||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Renders encoded AC-3/enhanced AC-3 data to an {@link AudioTrack} for decoding on the playback
|
||||
|
|
@ -105,8 +104,8 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
|
|||
this.source = Assertions.checkNotNull(source);
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
sampleHolder.data = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
|
||||
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
sampleHolder.replaceBuffer(DEFAULT_BUFFER_SIZE);
|
||||
formatHolder = new MediaFormatHolder();
|
||||
audioTrack = new AudioTrack();
|
||||
shouldReadInputBuffer = true;
|
||||
|
|
@ -199,8 +198,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
|
|||
|
||||
// Get more data if we have run out.
|
||||
if (shouldReadInputBuffer) {
|
||||
sampleHolder.data.clear();
|
||||
|
||||
sampleHolder.clearData();
|
||||
int result =
|
||||
source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.FORMAT_READ) {
|
||||
|
|
|
|||
|
|
@ -96,4 +96,13 @@ public final class SampleHolder {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears {@link #data}. Does nothing if {@link #data} is null.
|
||||
*/
|
||||
public void clearData() {
|
||||
if (data != null) {
|
||||
data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@ public class SubtitleParserHelper implements Handler.Callback {
|
|||
if (sampleHolder != holder) {
|
||||
// A flush has occurred since this holder was posted. Do nothing.
|
||||
} else {
|
||||
holder.data.position(0);
|
||||
this.result = result;
|
||||
this.error = error;
|
||||
this.parsing = false;
|
||||
|
|
|
|||
|
|
@ -177,8 +177,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||
if (!inputStreamEnded && subtitle == null) {
|
||||
try {
|
||||
SampleHolder sampleHolder = parserHelper.getSampleHolder();
|
||||
sampleHolder.clearData();
|
||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
if (result == SampleSource.SAMPLE_READ && !sampleHolder.decodeOnly) {
|
||||
parserHelper.startParseOperation();
|
||||
textRendererNeedsUpdate = false;
|
||||
} else if (result == SampleSource.END_OF_STREAM) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608;
|
|||
/**
|
||||
* A Closed Caption that contains textual data associated with time indices.
|
||||
*/
|
||||
public final class ClosedCaption implements Comparable<ClosedCaption> {
|
||||
/* package */ abstract class ClosedCaption implements Comparable<ClosedCaption> {
|
||||
|
||||
/**
|
||||
* Identifies closed captions with control characters.
|
||||
|
|
@ -30,23 +30,16 @@ public final class ClosedCaption implements Comparable<ClosedCaption> {
|
|||
public static final int TYPE_TEXT = 1;
|
||||
|
||||
/**
|
||||
* The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field
|
||||
* has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control
|
||||
* characters (C1, C2).
|
||||
* The type of the closed caption data.
|
||||
*/
|
||||
public final int type;
|
||||
/**
|
||||
* Contains text or two control characters.
|
||||
*/
|
||||
public final String text;
|
||||
/**
|
||||
* Timestamp associated with the closed caption.
|
||||
*/
|
||||
public final long timeUs;
|
||||
|
||||
public ClosedCaption(int type, String text, long timeUs) {
|
||||
protected ClosedCaption(int type, long timeUs) {
|
||||
this.type = type;
|
||||
this.text = text;
|
||||
this.timeUs = timeUs;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.text.eia608;
|
||||
|
||||
/* package */ final class ClosedCaptionCtrl extends ClosedCaption {
|
||||
|
||||
/**
|
||||
* The receipt of the {@link #RESUME_CAPTION_LOADING} command initiates pop-on style captioning.
|
||||
* Subsequent data should be loaded into a non-displayed memory and held there until the
|
||||
* {@link #END_OF_CAPTION} command is received, at which point the non-displayed memory becomes
|
||||
* the displayed memory (and vice versa).
|
||||
*/
|
||||
public static final byte RESUME_CAPTION_LOADING = 0x20;
|
||||
/**
|
||||
* The receipt of the {@link #ROLL_UP_CAPTIONS_2_ROWS} command initiates roll-up style
|
||||
* captioning, with the maximum of 2 rows displayed simultaneously.
|
||||
*/
|
||||
public static final byte ROLL_UP_CAPTIONS_2_ROWS = 0x25;
|
||||
/**
|
||||
* The receipt of the {@link #ROLL_UP_CAPTIONS_3_ROWS} command initiates roll-up style
|
||||
* captioning, with the maximum of 3 rows displayed simultaneously.
|
||||
*/
|
||||
public static final byte ROLL_UP_CAPTIONS_3_ROWS = 0x26;
|
||||
/**
|
||||
* The receipt of the {@link #ROLL_UP_CAPTIONS_4_ROWS} command initiates roll-up style
|
||||
* captioning, with the maximum of 4 rows displayed simultaneously.
|
||||
*/
|
||||
public static final byte ROLL_UP_CAPTIONS_4_ROWS = 0x27;
|
||||
/**
|
||||
* The receipt of the {@link #RESUME_DIRECT_CAPTIONING} command initiates paint-on style
|
||||
* captioning. Subsequent data should be addressed immediately to displayed memory without need
|
||||
* for the {@link #RESUME_CAPTION_LOADING} command.
|
||||
*/
|
||||
public static final byte RESUME_DIRECT_CAPTIONING = 0x29;
|
||||
/**
|
||||
* The receipt of the {@link #END_OF_CAPTION} command indicates the end of pop-on style caption,
|
||||
* at this point already loaded in non-displayed memory caption should become the displayed
|
||||
* memory (and vice versa). If no {@link #RESUME_CAPTION_LOADING} command has been received,
|
||||
* {@link #END_OF_CAPTION} command forces the receiver into pop-on style.
|
||||
*/
|
||||
public static final byte END_OF_CAPTION = 0x2F;
|
||||
|
||||
public static final byte ERASE_DISPLAYED_MEMORY = 0x2C;
|
||||
public static final byte CARRIAGE_RETURN = 0x2D;
|
||||
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E;
|
||||
|
||||
|
||||
public static final byte MID_ROW_CHAN_1 = 0x11;
|
||||
public static final byte MID_ROW_CHAN_2 = 0x19;
|
||||
|
||||
public static final byte MISC_CHAN_1 = 0x14;
|
||||
public static final byte MISC_CHAN_2 = 0x1C;
|
||||
|
||||
public static final byte TAB_OFFSET_CHAN_1 = 0x17;
|
||||
public static final byte TAB_OFFSET_CHAN_2 = 0x1F;
|
||||
|
||||
public final byte cc1;
|
||||
public final byte cc2;
|
||||
|
||||
protected ClosedCaptionCtrl(byte cc1, byte cc2, long timeUs) {
|
||||
super(ClosedCaption.TYPE_CTRL, timeUs);
|
||||
this.cc1 = cc1;
|
||||
this.cc2 = cc2;
|
||||
}
|
||||
|
||||
public boolean isMidRowCode() {
|
||||
return (cc1 == MID_ROW_CHAN_1 || cc1 == MID_ROW_CHAN_2) && (cc2 >= 0x20 && cc2 <= 0x2F);
|
||||
}
|
||||
|
||||
public boolean isMiscCode() {
|
||||
return (cc1 == MISC_CHAN_1 || cc1 == MISC_CHAN_2) && (cc2 >= 0x20 && cc2 <= 0x2F);
|
||||
}
|
||||
|
||||
public boolean isTabOffsetCode() {
|
||||
return (cc1 == TAB_OFFSET_CHAN_1 || cc1 == TAB_OFFSET_CHAN_2) && (cc2 >= 0x21 && cc2 <= 0x23);
|
||||
}
|
||||
|
||||
public boolean isPreambleAddressCode() {
|
||||
return (cc1 >= 0x10 && cc1 <= 0x1F) && (cc2 >= 0x40 && cc2 <= 0x7F);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.text.eia608;
|
||||
|
||||
/* package */ final class ClosedCaptionText extends ClosedCaption {
|
||||
|
||||
public final String text;
|
||||
|
||||
public ClosedCaptionText(String text, long timeUs) {
|
||||
super(ClosedCaption.TYPE_TEXT, timeUs);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,8 +18,6 @@ package com.google.android.exoplayer.text.eia608;
|
|||
import com.google.android.exoplayer.util.BitArray;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -82,22 +80,29 @@ public class Eia608Parser {
|
|||
0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
|
||||
};
|
||||
|
||||
public boolean canParse(String mimeType) {
|
||||
private final BitArray seiBuffer;
|
||||
private final StringBuilder stringBuilder;
|
||||
|
||||
/* package */ Eia608Parser() {
|
||||
seiBuffer = new BitArray();
|
||||
stringBuilder = new StringBuilder();
|
||||
}
|
||||
|
||||
/* package */ boolean canParse(String mimeType) {
|
||||
return mimeType.equals(MimeTypes.APPLICATION_EIA608);
|
||||
}
|
||||
|
||||
public List<ClosedCaption> parse(byte[] data, int size, long timeUs) {
|
||||
/* package */ void parse(byte[] data, int size, long timeUs, List<ClosedCaption> out) {
|
||||
if (size <= 0) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
BitArray seiBuffer = new BitArray(data, size);
|
||||
|
||||
stringBuilder.setLength(0);
|
||||
seiBuffer.reset(data, size);
|
||||
seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit
|
||||
int ccCount = seiBuffer.readBits(5);
|
||||
seiBuffer.skipBytes(1);
|
||||
|
||||
List<ClosedCaption> captions = new ArrayList<ClosedCaption>();
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < ccCount; i++) {
|
||||
seiBuffer.skipBits(5); // one_bit + reserved
|
||||
boolean ccValid = seiBuffer.readBit();
|
||||
|
|
@ -129,12 +134,10 @@ public class Eia608Parser {
|
|||
// Control character.
|
||||
if (ccData1 < 0x20) {
|
||||
if (stringBuilder.length() > 0) {
|
||||
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(),
|
||||
timeUs));
|
||||
out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs));
|
||||
stringBuilder.setLength(0);
|
||||
}
|
||||
captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL,
|
||||
new String(new char[] {(char) ccData1, (char) ccData2}), timeUs));
|
||||
out.add(new ClosedCaptionCtrl(ccData1, ccData2, timeUs));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -146,10 +149,8 @@ public class Eia608Parser {
|
|||
}
|
||||
|
||||
if (stringBuilder.length() > 0) {
|
||||
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), timeUs));
|
||||
out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(captions);
|
||||
}
|
||||
|
||||
private static char getChar(byte ccData) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer.text.eia608;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.MediaFormatHolder;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
|
|
@ -22,6 +23,7 @@ import com.google.android.exoplayer.SampleSource;
|
|||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.text.TextRenderer;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Handler.Callback;
|
||||
|
|
@ -29,10 +31,8 @@ import android.os.Looper;
|
|||
import android.os.Message;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* A {@link TrackRenderer} for EIA-608 closed captions in a media stream.
|
||||
|
|
@ -40,26 +40,32 @@ import java.util.Queue;
|
|||
public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
||||
|
||||
private static final int MSG_INVOKE_RENDERER = 0;
|
||||
// The Number of closed captions text line to keep in memory.
|
||||
private static final int ALLOWED_CAPTIONS_TEXT_LINES_COUNT = 4;
|
||||
|
||||
private static final int CC_MODE_UNKNOWN = 0;
|
||||
private static final int CC_MODE_ROLL_UP = 1;
|
||||
private static final int CC_MODE_POP_ON = 2;
|
||||
private static final int CC_MODE_PAINT_ON = 3;
|
||||
|
||||
// The default number of rows to display in roll-up captions mode.
|
||||
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
|
||||
|
||||
private final SampleSource source;
|
||||
private final Eia608Parser eia608Parser;
|
||||
private final TextRenderer textRenderer;
|
||||
private final Handler metadataHandler;
|
||||
private final Handler textRendererHandler;
|
||||
private final MediaFormatHolder formatHolder;
|
||||
private final SampleHolder sampleHolder;
|
||||
private final StringBuilder closedCaptionStringBuilder;
|
||||
//Currently displayed captions.
|
||||
private final List<ClosedCaption> currentCaptions;
|
||||
private final Queue<Integer> newLineIndexes;
|
||||
private final StringBuilder captionStringBuilder;
|
||||
private final List<ClosedCaption> captionBuffer;
|
||||
|
||||
private int trackIndex;
|
||||
private long currentPositionUs;
|
||||
private boolean inputStreamEnded;
|
||||
|
||||
private long pendingCaptionsTimestamp;
|
||||
private List<ClosedCaption> pendingCaptions;
|
||||
private int captionMode;
|
||||
private int captionRowCount;
|
||||
private String caption;
|
||||
private String lastRenderedCaption;
|
||||
|
||||
/**
|
||||
* @param source A source from which samples containing EIA-608 closed captions can be read.
|
||||
|
|
@ -74,14 +80,12 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
Looper textRendererLooper) {
|
||||
this.source = Assertions.checkNotNull(source);
|
||||
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
||||
this.metadataHandler = textRendererLooper == null ? null
|
||||
: new Handler(textRendererLooper, this);
|
||||
textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this);
|
||||
eia608Parser = new Eia608Parser();
|
||||
formatHolder = new MediaFormatHolder();
|
||||
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
closedCaptionStringBuilder = new StringBuilder();
|
||||
currentCaptions = new LinkedList<ClosedCaption>();
|
||||
newLineIndexes = new LinkedList<Integer>();
|
||||
captionStringBuilder = new StringBuilder();
|
||||
captionBuffer = new ArrayList<ClosedCaption>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -117,10 +121,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
|
||||
private void seekToInternal(long positionUs) {
|
||||
currentPositionUs = positionUs;
|
||||
pendingCaptions = null;
|
||||
inputStreamEnded = false;
|
||||
// Clear displayed captions.
|
||||
currentCaptions.clear();
|
||||
clearPendingSample();
|
||||
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
|
||||
setCaptionMode(CC_MODE_UNKNOWN);
|
||||
invokeRenderer(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -133,15 +138,10 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
|
||||
if (!inputStreamEnded && pendingCaptions == null) {
|
||||
if (!inputStreamEnded && !isSamplePending()) {
|
||||
try {
|
||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
pendingCaptionsTimestamp = sampleHolder.timeUs;
|
||||
pendingCaptions = eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size,
|
||||
sampleHolder.timeUs);
|
||||
sampleHolder.data.clear();
|
||||
} else if (result == SampleSource.END_OF_STREAM) {
|
||||
if (result == SampleSource.END_OF_STREAM) {
|
||||
inputStreamEnded = true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
|
@ -149,15 +149,22 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
}
|
||||
}
|
||||
|
||||
if (pendingCaptions != null && pendingCaptionsTimestamp <= currentPositionUs) {
|
||||
invokeRenderer(pendingCaptions);
|
||||
pendingCaptions = null;
|
||||
if (isSamplePending() && sampleHolder.timeUs <= currentPositionUs) {
|
||||
// Parse the pending sample.
|
||||
eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size, sampleHolder.timeUs,
|
||||
captionBuffer);
|
||||
// Consume parsed captions.
|
||||
consumeCaptionBuffer();
|
||||
// Update the renderer, unless the sample was marked for decoding only.
|
||||
if (!sampleHolder.decodeOnly) {
|
||||
invokeRenderer(caption);
|
||||
}
|
||||
clearPendingSample();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
pendingCaptions = null;
|
||||
source.disable(trackIndex);
|
||||
}
|
||||
|
||||
|
|
@ -186,11 +193,16 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
return true;
|
||||
}
|
||||
|
||||
private void invokeRenderer(List<ClosedCaption> metadata) {
|
||||
if (metadataHandler != null) {
|
||||
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
|
||||
private void invokeRenderer(String text) {
|
||||
if (Util.areEqual(lastRenderedCaption, text)) {
|
||||
// No change.
|
||||
return;
|
||||
}
|
||||
this.lastRenderedCaption = text;
|
||||
if (textRendererHandler != null) {
|
||||
textRendererHandler.obtainMessage(MSG_INVOKE_RENDERER, text).sendToTarget();
|
||||
} else {
|
||||
invokeRendererInternal(metadata);
|
||||
invokeRendererInternal(text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,62 +211,155 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||
public boolean handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_INVOKE_RENDERER:
|
||||
invokeRendererInternal((List<ClosedCaption>) msg.obj);
|
||||
invokeRendererInternal((String) msg.obj);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void invokeRendererInternal(List<ClosedCaption> metadata) {
|
||||
currentCaptions.addAll(metadata);
|
||||
// Sort captions by the timestamp.
|
||||
Collections.sort(currentCaptions);
|
||||
closedCaptionStringBuilder.setLength(0);
|
||||
private void invokeRendererInternal(String text) {
|
||||
textRenderer.onText(text);
|
||||
}
|
||||
|
||||
// After processing keep only captions after cutIndex.
|
||||
int cutIndex = 0;
|
||||
newLineIndexes.clear();
|
||||
for (int i = 0; i < currentCaptions.size(); i++) {
|
||||
ClosedCaption caption = currentCaptions.get(i);
|
||||
private void consumeCaptionBuffer() {
|
||||
int captionBufferSize = captionBuffer.size();
|
||||
if (captionBufferSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < captionBufferSize; i++) {
|
||||
ClosedCaption caption = captionBuffer.get(i);
|
||||
if (caption.type == ClosedCaption.TYPE_CTRL) {
|
||||
int cc2 = caption.text.codePointAt(1);
|
||||
switch (cc2) {
|
||||
case 0x2C: // Erase Displayed Memory.
|
||||
closedCaptionStringBuilder.setLength(0);
|
||||
cutIndex = i;
|
||||
newLineIndexes.clear();
|
||||
break;
|
||||
case 0x25: // Roll-Up.
|
||||
case 0x26:
|
||||
case 0x27:
|
||||
default:
|
||||
if (cc2 >= 0x20 && cc2 < 0x40) {
|
||||
break;
|
||||
}
|
||||
if (closedCaptionStringBuilder.length() > 0
|
||||
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1)
|
||||
!= '\n') {
|
||||
closedCaptionStringBuilder.append('\n');
|
||||
newLineIndexes.add(i);
|
||||
if (newLineIndexes.size() >= ALLOWED_CAPTIONS_TEXT_LINES_COUNT) {
|
||||
cutIndex = newLineIndexes.poll();
|
||||
}
|
||||
}
|
||||
break;
|
||||
ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption;
|
||||
if (captionCtrl.isMiscCode()) {
|
||||
handleMiscCode(captionCtrl);
|
||||
} else if (captionCtrl.isPreambleAddressCode()) {
|
||||
handlePreambleAddressCode();
|
||||
}
|
||||
} else {
|
||||
closedCaptionStringBuilder.append(caption.text);
|
||||
handleText((ClosedCaptionText) caption);
|
||||
}
|
||||
}
|
||||
captionBuffer.clear();
|
||||
|
||||
if (cutIndex > 0 && cutIndex < currentCaptions.size() - 1) {
|
||||
for (int i = 0; i <= cutIndex; i++) {
|
||||
currentCaptions.remove(0);
|
||||
}
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
|
||||
caption = getDisplayCaption();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleText(ClosedCaptionText captionText) {
|
||||
if (captionMode != CC_MODE_UNKNOWN) {
|
||||
captionStringBuilder.append(captionText.text);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMiscCode(ClosedCaptionCtrl captionCtrl) {
|
||||
switch (captionCtrl.cc2) {
|
||||
case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_2_ROWS:
|
||||
captionRowCount = 2;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
return;
|
||||
case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_3_ROWS:
|
||||
captionRowCount = 3;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
return;
|
||||
case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_4_ROWS:
|
||||
captionRowCount = 4;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
return;
|
||||
case ClosedCaptionCtrl.RESUME_CAPTION_LOADING:
|
||||
setCaptionMode(CC_MODE_POP_ON);
|
||||
return;
|
||||
case ClosedCaptionCtrl.RESUME_DIRECT_CAPTIONING:
|
||||
setCaptionMode(CC_MODE_PAINT_ON);
|
||||
return;
|
||||
}
|
||||
|
||||
textRenderer.onText(closedCaptionStringBuilder.toString());
|
||||
if (captionMode == CC_MODE_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (captionCtrl.cc2) {
|
||||
case ClosedCaptionCtrl.ERASE_DISPLAYED_MEMORY:
|
||||
caption = null;
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
|
||||
captionStringBuilder.setLength(0);
|
||||
}
|
||||
return;
|
||||
case ClosedCaptionCtrl.ERASE_NON_DISPLAYED_MEMORY:
|
||||
captionStringBuilder.setLength(0);
|
||||
return;
|
||||
case ClosedCaptionCtrl.END_OF_CAPTION:
|
||||
caption = getDisplayCaption();
|
||||
captionStringBuilder.setLength(0);
|
||||
return;
|
||||
case ClosedCaptionCtrl.CARRIAGE_RETURN:
|
||||
maybeAppendNewline();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePreambleAddressCode() {
|
||||
// TODO: Add better handling of this with specific positioning.
|
||||
maybeAppendNewline();
|
||||
}
|
||||
|
||||
private void setCaptionMode(int captionMode) {
|
||||
if (this.captionMode == captionMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.captionMode = captionMode;
|
||||
// Clear the working memory.
|
||||
captionStringBuilder.setLength(0);
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) {
|
||||
// When switching to roll-up or unknown, we also need to clear the caption.
|
||||
caption = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeAppendNewline() {
|
||||
int buildLength = captionStringBuilder.length();
|
||||
if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') {
|
||||
captionStringBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
private String getDisplayCaption() {
|
||||
int buildLength = captionStringBuilder.length();
|
||||
if (buildLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean endsWithNewline = captionStringBuilder.charAt(buildLength - 1) == '\n';
|
||||
if (buildLength == 1 && endsWithNewline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int endIndex = endsWithNewline ? buildLength - 1 : buildLength;
|
||||
if (captionMode != CC_MODE_ROLL_UP) {
|
||||
return captionStringBuilder.substring(0, endIndex);
|
||||
}
|
||||
|
||||
int startIndex = 0;
|
||||
int searchBackwardFromIndex = endIndex;
|
||||
for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) {
|
||||
searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1);
|
||||
}
|
||||
if (searchBackwardFromIndex != -1) {
|
||||
startIndex = searchBackwardFromIndex + 1;
|
||||
}
|
||||
captionStringBuilder.delete(0, startIndex);
|
||||
return captionStringBuilder.substring(0, endIndex - startIndex);
|
||||
}
|
||||
|
||||
private void clearPendingSample() {
|
||||
sampleHolder.timeUs = C.UNKNOWN_TIME_US;
|
||||
sampleHolder.clearData();
|
||||
}
|
||||
|
||||
private boolean isSamplePending() {
|
||||
return sampleHolder.timeUs != C.UNKNOWN_TIME_US;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue