Commit a03f8a1c by Ian Baker

Merge pull request #7199 from TiVo:p-fix-stuckcaption

PiperOrigin-RevId: 308229206
parent bf5b52e2
...@@ -101,6 +101,9 @@ ...@@ -101,6 +101,9 @@
used `start`, `middle` and `end`). used `start`, `middle` and `end`).
* Use anti-aliasing and bitmap filtering when displaying bitmap subtitles * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles
([#6950](https://github.com/google/ExoPlayer/pull/6950)). ([#6950](https://github.com/google/ExoPlayer/pull/6950)).
* Implement timing-out of stuck CEA-608 captions (as permitted by
ANSI/CTA-608-E R-2014 Annex C.9) and set the default timeout to 16
seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)).
* DRM: * DRM:
* Add support for attaching DRM sessions to clear content in the demo app. * Add support for attaching DRM sessions to clear content in the demo app.
* Remove `DrmSessionManager` references from all renderers. * Remove `DrmSessionManager` references from all renderers.
...@@ -111,8 +114,8 @@ ...@@ -111,8 +114,8 @@
([#7078](https://github.com/google/ExoPlayer/issues/7078)). ([#7078](https://github.com/google/ExoPlayer/issues/7078)).
* Remove generics from DRM components. * Remove generics from DRM components.
* Downloads and caching: * Downloads and caching:
* Merge downloads in `SegmentDownloader` to improve overall download * Merge downloads in `SegmentDownloader` to improve overall download speed
speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
`CacheDataSink.Factory` and `CacheDataSource.Factory` respectively. `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
* Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory` * Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory`
......
...@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.text.ttml.TtmlDecoder; ...@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder; import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;
import com.google.android.exoplayer2.text.webvtt.WebvttDecoder; import com.google.android.exoplayer2.text.webvtt.WebvttDecoder;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
/** /**
...@@ -109,8 +108,10 @@ public interface SubtitleDecoderFactory { ...@@ -109,8 +108,10 @@ public interface SubtitleDecoderFactory {
return new Tx3gDecoder(format.initializationData); return new Tx3gDecoder(format.initializationData);
case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_CEA608:
case MimeTypes.APPLICATION_MP4CEA608: case MimeTypes.APPLICATION_MP4CEA608:
return new Cea608Decoder(mimeType, format.accessibilityChannel, return new Cea608Decoder(
16000L, Clock.DEFAULT); mimeType,
format.accessibilityChannel,
Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS);
case MimeTypes.APPLICATION_CEA708: case MimeTypes.APPLICATION_CEA708:
return new Cea708Decoder(format.accessibilityChannel, format.initializationData); return new Cea708Decoder(format.accessibilityChannel, format.initializationData);
case MimeTypes.APPLICATION_DVBSUBS: case MimeTypes.APPLICATION_DVBSUBS:
......
...@@ -26,12 +26,14 @@ import android.text.style.StyleSpan; ...@@ -26,12 +26,14 @@ import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -41,11 +43,15 @@ import java.util.Collections; ...@@ -41,11 +43,15 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /** A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). */
* A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
*/
public final class Cea608Decoder extends CeaDecoder { public final class Cea608Decoder extends CeaDecoder {
/**
* The minimum value for the {@code validDataChannelTimeoutMs} constructor parameter permitted by
* ANSI/CTA-608-E R-2014 Annex C.9.
*/
public static final long MIN_DATA_CHANNEL_TIMEOUT_MS = 16_000;
private static final String TAG = "Cea608Decoder"; private static final String TAG = "Cea608Decoder";
private static final int CC_VALID_FLAG = 0x04; private static final int CC_VALID_FLAG = 0x04;
...@@ -238,6 +244,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -238,6 +244,7 @@ public final class Cea608Decoder extends CeaDecoder {
private final int packetLength; private final int packetLength;
private final int selectedField; private final int selectedField;
private final int selectedChannel; private final int selectedChannel;
private final long validDataChannelTimeoutUs;
private final ArrayList<CueBuilder> cueBuilders; private final ArrayList<CueBuilder> cueBuilders;
private CueBuilder currentCueBuilder; private CueBuilder currentCueBuilder;
...@@ -258,26 +265,26 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -258,26 +265,26 @@ public final class Cea608Decoder extends CeaDecoder {
// service bytes and drops the rest. // service bytes and drops the rest.
private boolean isInCaptionService; private boolean isInCaptionService;
// Static counter to keep track of last CC rendered. This is used to force erase the caption when private long lastCueUpdateUs;
// the stream does not explicitly send control codes to remove caption as specified by
// CEA-608 Annex C.9
private long lastCueUpdateMs = C.TIME_UNSET;
private boolean captionEraseCommandSeen = false;
// CEA-608 Annex C.9 propose that if no data are received for the selected caption channel within
// a given time, the decoder should automatically erase the caption. The time limit should be no
// less than 16 seconds
// This value is set in the constructor. The automatic erasure is disabled when this value is 0
private long validDataChannelTimeoutMs = 0;
private Clock clock;
public Cea608Decoder(String mimeType, int accessibilityChannel, long timeoutMs, Clock clock) { /**
* Constructs an instance.
*
* @param mimeType The MIME type of the CEA-608 data.
* @param accessibilityChannel The Accessibility channel, or {@link
* com.google.android.exoplayer2.Format#NO_VALUE} if unknown.
* @param validDataChannelTimeoutMs The timeout (in milliseconds) permitted by ANSI/CTA-608-E
* R-2014 Annex C.9 to clear "stuck" captions where no removal control code is received. The
* timeout should be at least {@link #MIN_DATA_CHANNEL_TIMEOUT_MS} or {@link C#TIME_UNSET} for
* no timeout.
*/
public Cea608Decoder(String mimeType, int accessibilityChannel, long validDataChannelTimeoutMs) {
ccData = new ParsableByteArray(); ccData = new ParsableByteArray();
cueBuilders = new ArrayList<>(); cueBuilders = new ArrayList<>();
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
currentChannel = NTSC_CC_CHANNEL_1; currentChannel = NTSC_CC_CHANNEL_1;
validDataChannelTimeoutMs = timeoutMs; this.validDataChannelTimeoutUs =
this.clock = clock; validDataChannelTimeoutMs > 0 ? validDataChannelTimeoutMs * 1000 : C.TIME_UNSET;
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
switch (accessibilityChannel) { switch (accessibilityChannel) {
case 1: case 1:
...@@ -305,6 +312,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -305,6 +312,7 @@ public final class Cea608Decoder extends CeaDecoder {
setCaptionMode(CC_MODE_UNKNOWN); setCaptionMode(CC_MODE_UNKNOWN);
resetCueBuilders(); resetCueBuilders();
isInCaptionService = true; isInCaptionService = true;
lastCueUpdateUs = C.TIME_UNSET;
} }
@Override @Override
...@@ -326,7 +334,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -326,7 +334,7 @@ public final class Cea608Decoder extends CeaDecoder {
repeatableControlCc2 = 0; repeatableControlCc2 = 0;
currentChannel = NTSC_CC_CHANNEL_1; currentChannel = NTSC_CC_CHANNEL_1;
isInCaptionService = true; isInCaptionService = true;
lastCueUpdateMs = C.TIME_UNSET; lastCueUpdateUs = C.TIME_UNSET;
} }
@Override @Override
...@@ -334,6 +342,26 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -334,6 +342,26 @@ public final class Cea608Decoder extends CeaDecoder {
// Do nothing // Do nothing
} }
@Nullable
@Override
public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
SubtitleOutputBuffer outputBuffer = super.dequeueOutputBuffer();
if (outputBuffer != null) {
return outputBuffer;
}
if (shouldClearStuckCaptions()) {
outputBuffer = getAvailableOutputBuffer();
if (outputBuffer != null) {
cues = Collections.emptyList();
lastCueUpdateUs = C.TIME_UNSET;
Subtitle subtitle = createSubtitle();
outputBuffer.setContent(getPositionUs(), subtitle, Format.OFFSET_SAMPLE_RELATIVE);
return outputBuffer;
}
}
return null;
}
@Override @Override
protected boolean isNewSubtitleDataAvailable() { protected boolean isNewSubtitleDataAvailable() {
return cues != lastCues; return cues != lastCues;
...@@ -351,7 +379,6 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -351,7 +379,6 @@ public final class Cea608Decoder extends CeaDecoder {
ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data); ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data);
ccData.reset(subtitleData.array(), subtitleData.limit()); ccData.reset(subtitleData.array(), subtitleData.limit());
boolean captionDataProcessed = false; boolean captionDataProcessed = false;
captionEraseCommandSeen = false;
while (ccData.bytesLeft() >= packetLength) { while (ccData.bytesLeft() >= packetLength) {
byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER
: (byte) ccData.readUnsignedByte(); : (byte) ccData.readUnsignedByte();
...@@ -361,6 +388,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -361,6 +388,7 @@ public final class Cea608Decoder extends CeaDecoder {
// TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
// to the CEA-608 specification. We need to determine if the data should be handled // to the CEA-608 specification. We need to determine if the data should be handled
// differently when that is not the case. // differently when that is not the case.
if ((ccHeader & CC_TYPE_FLAG) != 0) { if ((ccHeader & CC_TYPE_FLAG) != 0) {
// Do not process anything that is not part of the 608 byte stream. // Do not process anything that is not part of the 608 byte stream.
continue; continue;
...@@ -370,6 +398,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -370,6 +398,7 @@ public final class Cea608Decoder extends CeaDecoder {
// Do not process packets not within the selected field. // Do not process packets not within the selected field.
continue; continue;
} }
// Strip the parity bit from each byte to get CC data. // Strip the parity bit from each byte to get CC data.
byte ccData1 = (byte) (ccByte1 & 0x7F); byte ccData1 = (byte) (ccByte1 & 0x7F);
byte ccData2 = (byte) (ccByte2 & 0x7F); byte ccData2 = (byte) (ccByte2 & 0x7F);
...@@ -439,9 +468,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -439,9 +468,7 @@ public final class Cea608Decoder extends CeaDecoder {
if (captionDataProcessed) { if (captionDataProcessed) {
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
cues = getDisplayCues(); cues = getDisplayCues();
if ((validDataChannelTimeoutMs != 0) && !captionEraseCommandSeen) { lastCueUpdateUs = getPositionUs();
lastCueUpdateMs = clock.elapsedRealtime();
}
} }
} }
} }
...@@ -560,17 +587,14 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -560,17 +587,14 @@ public final class Cea608Decoder extends CeaDecoder {
cues = Collections.emptyList(); cues = Collections.emptyList();
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
resetCueBuilders(); resetCueBuilders();
captionEraseCommandSeen = true;
} }
break; break;
case CTRL_ERASE_NON_DISPLAYED_MEMORY: case CTRL_ERASE_NON_DISPLAYED_MEMORY:
resetCueBuilders(); resetCueBuilders();
captionEraseCommandSeen = true;
break; break;
case CTRL_END_OF_CAPTION: case CTRL_END_OF_CAPTION:
cues = getDisplayCues(); cues = getDisplayCues();
resetCueBuilders(); resetCueBuilders();
captionEraseCommandSeen = true;
break; break;
case CTRL_CARRIAGE_RETURN: case CTRL_CARRIAGE_RETURN:
// carriage returns only apply to rollup captions; don't bother if we don't have anything // carriage returns only apply to rollup captions; don't bother if we don't have anything
...@@ -1040,17 +1064,12 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -1040,17 +1064,12 @@ public final class Cea608Decoder extends CeaDecoder {
} }
protected void clearStuckCaptions() /** See ANSI/CTA-608-E R-2014 Annex C.9 for Caption Erase Logic. */
{ private boolean shouldClearStuckCaptions() {
if ((validDataChannelTimeoutMs != 0) && if (validDataChannelTimeoutUs == C.TIME_UNSET || lastCueUpdateUs == C.TIME_UNSET) {
(lastCueUpdateMs != C.TIME_UNSET)) { return false;
long timeElapsed = clock.elapsedRealtime() - lastCueUpdateMs; }
if (timeElapsed >= validDataChannelTimeoutMs) { long elapsedUs = getPositionUs() - lastCueUpdateUs;
// Force erase captions. There might be stale captions stuck on screen. return elapsedUs >= validDataChannelTimeoutUs;
// (CEA-608 Annex C.9)
cues = Collections.emptyList();
lastCueUpdateMs = C.TIME_UNSET;
}
}
} }
} }
...@@ -1296,14 +1296,6 @@ public final class Cea708Decoder extends CeaDecoder { ...@@ -1296,14 +1296,6 @@ public final class Cea708Decoder extends CeaDecoder {
} }
} }
protected void clearStuckCaptions()
{
// Do nothing for CEA-708.
// As per spec CEA-708 Caption text sequences shall be terminated by either the start of a new
// DTVCC Command, or with an ASCII ETX (End of Text) (0x03) character when no other DTVCC
// Commands follow.
}
/** A {@link Cue} for CEA-708. */ /** A {@link Cue} for CEA-708. */
private static final class Cea708CueInfo { private static final class Cea708CueInfo {
......
...@@ -97,8 +97,6 @@ import java.util.PriorityQueue; ...@@ -97,8 +97,6 @@ import java.util.PriorityQueue;
if (availableOutputBuffers.isEmpty()) { if (availableOutputBuffers.isEmpty()) {
return null; return null;
} }
// check if 608 decoder needs to clean up the stale caption
clearStuckCaptions();
// iterate through all available input buffers whose timestamps are less than or equal // iterate through all available input buffers whose timestamps are less than or equal
// to the current playback position; processing input buffers for future content should // to the current playback position; processing input buffers for future content should
// be deferred until they would be applicable // be deferred until they would be applicable
...@@ -181,6 +179,15 @@ import java.util.PriorityQueue; ...@@ -181,6 +179,15 @@ import java.util.PriorityQueue;
*/ */
protected abstract void decode(SubtitleInputBuffer inputBuffer); protected abstract void decode(SubtitleInputBuffer inputBuffer);
@Nullable
protected final SubtitleOutputBuffer getAvailableOutputBuffer() {
return availableOutputBuffers.pollFirst();
}
protected final long getPositionUs() {
return playbackPositionUs;
}
private static final class CeaInputBuffer extends SubtitleInputBuffer private static final class CeaInputBuffer extends SubtitleInputBuffer
implements Comparable<CeaInputBuffer> { implements Comparable<CeaInputBuffer> {
...@@ -215,9 +222,4 @@ import java.util.PriorityQueue; ...@@ -215,9 +222,4 @@ import java.util.PriorityQueue;
owner.releaseOutputBuffer(this); owner.releaseOutputBuffer(this);
} }
} }
/**
* Implements CEA-608 Annex C.9 automatic Caption Erase Logic
*/
protected abstract void clearStuckCaptions();
} }
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