mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Merge pull request #7199 from TiVo:p-fix-stuckcaption
PiperOrigin-RevId: 308229206
This commit is contained in:
parent
bf5b52e288
commit
a03f8a1c95
5 changed files with 76 additions and 59 deletions
|
|
@ -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 @@
|
||||||
([#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;
|
||||||
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 {
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
// 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;
|
* Constructs an instance.
|
||||||
private Clock clock;
|
*
|
||||||
|
* @param mimeType The MIME type of the CEA-608 data.
|
||||||
public Cea608Decoder(String mimeType, int accessibilityChannel, long timeoutMs, Clock clock) {
|
* @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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
// 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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
// Force erase captions. There might be stale captions stuck on screen.
|
|
||||||
// (CEA-608 Annex C.9)
|
|
||||||
cues = Collections.emptyList();
|
|
||||||
lastCueUpdateMs = C.TIME_UNSET;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
long elapsedUs = getPositionUs() - lastCueUpdateUs;
|
||||||
|
return elapsedUs >= validDataChannelTimeoutUs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
owner.releaseOutputBuffer(this);
|
owner.releaseOutputBuffer(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements CEA-608 Annex C.9 automatic Caption Erase Logic
|
|
||||||
*/
|
|
||||||
protected abstract void clearStuckCaptions();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue