diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 36e5332b4a..66641aae93 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -101,6 +101,9 @@ used `start`, `middle` and `end`). * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles ([#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: * Add support for attaching DRM sessions to clear content in the demo app. * Remove `DrmSessionManager` references from all renderers. @@ -111,8 +114,8 @@ ([#7078](https://github.com/google/ExoPlayer/issues/7078)). * Remove generics from DRM components. * Downloads and caching: - * Merge downloads in `SegmentDownloader` to improve overall download - speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). + * Merge downloads in `SegmentDownloader` to improve overall download speed + ([#5978](https://github.com/google/ExoPlayer/issues/5978)). * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively. * Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory` diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index b7472b9630..bd652c6586 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -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.webvtt.Mp4WebvttDecoder; import com.google.android.exoplayer2.text.webvtt.WebvttDecoder; -import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -109,8 +108,10 @@ public interface SubtitleDecoderFactory { return new Tx3gDecoder(format.initializationData); case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(mimeType, format.accessibilityChannel, - 16000L, Clock.DEFAULT); + return new Cea608Decoder( + mimeType, + format.accessibilityChannel, + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); case MimeTypes.APPLICATION_CEA708: return new Cea708Decoder(format.accessibilityChannel, format.initializationData); case MimeTypes.APPLICATION_DVBSUBS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 0d667f96bf..75e86c4113 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -26,12 +26,14 @@ import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.annotation.Nullable; 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.Subtitle; 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.SubtitleOutputBuffer; 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.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -41,11 +43,15 @@ import java.util.Collections; import java.util.List; 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 { + /** + * 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 int CC_VALID_FLAG = 0x04; @@ -238,6 +244,7 @@ public final class Cea608Decoder extends CeaDecoder { private final int packetLength; private final int selectedField; private final int selectedChannel; + private final long validDataChannelTimeoutUs; private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; @@ -258,26 +265,26 @@ public final class Cea608Decoder extends CeaDecoder { // service bytes and drops the rest. private boolean isInCaptionService; - // Static counter to keep track of last CC rendered. This is used to force erase the caption when - // 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 + private long lastCueUpdateUs; - // 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(); cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); currentChannel = NTSC_CC_CHANNEL_1; - validDataChannelTimeoutMs = timeoutMs; - this.clock = clock; + this.validDataChannelTimeoutUs = + validDataChannelTimeoutMs > 0 ? validDataChannelTimeoutMs * 1000 : C.TIME_UNSET; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { case 1: @@ -305,6 +312,7 @@ public final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); resetCueBuilders(); isInCaptionService = true; + lastCueUpdateUs = C.TIME_UNSET; } @Override @@ -326,7 +334,7 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc2 = 0; currentChannel = NTSC_CC_CHANNEL_1; isInCaptionService = true; - lastCueUpdateMs = C.TIME_UNSET; + lastCueUpdateUs = C.TIME_UNSET; } @Override @@ -334,6 +342,26 @@ public final class Cea608Decoder extends CeaDecoder { // 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 protected boolean isNewSubtitleDataAvailable() { return cues != lastCues; @@ -351,7 +379,6 @@ public final class Cea608Decoder extends CeaDecoder { ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data); ccData.reset(subtitleData.array(), subtitleData.limit()); boolean captionDataProcessed = false; - captionEraseCommandSeen = false; while (ccData.bytesLeft() >= packetLength) { byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER : (byte) ccData.readUnsignedByte(); @@ -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 // to the CEA-608 specification. We need to determine if the data should be handled // differently when that is not the case. + if ((ccHeader & CC_TYPE_FLAG) != 0) { // Do not process anything that is not part of the 608 byte stream. continue; @@ -370,6 +398,7 @@ public final class Cea608Decoder extends CeaDecoder { // Do not process packets not within the selected field. continue; } + // Strip the parity bit from each byte to get CC data. byte ccData1 = (byte) (ccByte1 & 0x7F); byte ccData2 = (byte) (ccByte2 & 0x7F); @@ -439,9 +468,7 @@ public final class Cea608Decoder extends CeaDecoder { if (captionDataProcessed) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { cues = getDisplayCues(); - if ((validDataChannelTimeoutMs != 0) && !captionEraseCommandSeen) { - lastCueUpdateMs = clock.elapsedRealtime(); - } + lastCueUpdateUs = getPositionUs(); } } } @@ -560,17 +587,14 @@ public final class Cea608Decoder extends CeaDecoder { cues = Collections.emptyList(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { resetCueBuilders(); - captionEraseCommandSeen = true; } break; case CTRL_ERASE_NON_DISPLAYED_MEMORY: resetCueBuilders(); - captionEraseCommandSeen = true; break; case CTRL_END_OF_CAPTION: cues = getDisplayCues(); resetCueBuilders(); - captionEraseCommandSeen = true; break; case CTRL_CARRIAGE_RETURN: // 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 { } - protected void clearStuckCaptions() - { - if ((validDataChannelTimeoutMs != 0) && - (lastCueUpdateMs != C.TIME_UNSET)) { - long timeElapsed = clock.elapsedRealtime() - lastCueUpdateMs; - if (timeElapsed >= validDataChannelTimeoutMs) { - // Force erase captions. There might be stale captions stuck on screen. - // (CEA-608 Annex C.9) - cues = Collections.emptyList(); - lastCueUpdateMs = C.TIME_UNSET; - } - } + /** See ANSI/CTA-608-E R-2014 Annex C.9 for Caption Erase Logic. */ + private boolean shouldClearStuckCaptions() { + if (validDataChannelTimeoutUs == C.TIME_UNSET || lastCueUpdateUs == C.TIME_UNSET) { + return false; + } + long elapsedUs = getPositionUs() - lastCueUpdateUs; + return elapsedUs >= validDataChannelTimeoutUs; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 53d930c948..182fe7a2fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -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. */ private static final class Cea708CueInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index 8be6ff1312..f42b2a99cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -97,8 +97,6 @@ import java.util.PriorityQueue; if (availableOutputBuffers.isEmpty()) { 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 // to the current playback position; processing input buffers for future content should // be deferred until they would be applicable @@ -181,6 +179,15 @@ import java.util.PriorityQueue; */ 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 implements Comparable { @@ -215,9 +222,4 @@ import java.util.PriorityQueue; owner.releaseOutputBuffer(this); } } - - /** - * Implements CEA-608 Annex C.9 automatic Caption Erase Logic - */ - protected abstract void clearStuckCaptions(); }