From 8c0f7827e262c7c4609e0b677a173a1e71ef2bb0 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 26 Oct 2022 11:09:04 +0000 Subject: [PATCH] Fix `Cea608Decoder` handling of service switch commands in field 2 From ANSI-CTA-608-E R-2014 section 8.4: > When closed captioning is used on line 21, field 2, it shall conform > to all of the applicable specifications and recommended practices as > defined for field 1 services with the following differences: > 1. The non-printing character of the miscellaneous control-character > pairs that fall in the range of 0x14, 0x20 to 0x14, 0x2F in field 1, > shall be replaced with 0x15, 0x20 to 0x15, 0x2F when used in field > 2. > 2. The non-printing character of the miscellaneous control-character > pairs that fall in the range of 0x1C, 0x20 to 0x1C, 0x2F in field > 1, shall be replaced with 0x1D, 0x20 to 0x1D, 0x2F when used in > field 2. This basically means that `cc1=0x15` in field 2 should be interpreted as `cc1=0x14` in field 1, and same for `0x1D -> 0x1C`. The `isMiscCode` method above already handles this by ignoring the LSB (the only difference between `0x14` and `0x15`, and `0x1C` and `0x1D`) by AND-ing with `0xF6` instead of `0xF7`. This change uses the same trick in `isServiceSwitchCommand`. Issue: google/ExoPlayer#10666 #minor-release PiperOrigin-RevId: 483927506 --- RELEASENOTES.md | 3 + .../extractor/text/cea/Cea608Decoder.java | 4 +- .../extractor/text/cea/Cea608DecoderTest.java | 59 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9b36cdd98e..0d6bad7a66 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,9 @@ Release notes a non-empty but invalid license URL. * Fix `setMediaDrmSession failed: session not opened` error when switching between DRM schemes in a playlist (e.g. Widevine to ClearKey). +* Text: + * CEA-608: Ensure service switch commands on field 2 are handled correctly + ([#10666](https://github.com/google/ExoPlayer/issues/10666)). * DASH: * Parse `EventStream.presentationTimeOffset` from manifests ([#10460](https://github.com/google/ExoPlayer/issues/10460)). diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java index b73e303825..9a975939af 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea608Decoder.java @@ -874,8 +874,8 @@ private static boolean isXdsControlCode(byte cc1) { } private static boolean isServiceSwitchCommand(byte cc1) { - // cc1 - 0|0|0|1|C|1|0|0 - return (cc1 & 0xF7) == 0x14; + // cc1 - 0|0|0|1|C|1|0|F + return (cc1 & 0xF6) == 0x14; } private static final class CueBuilder { diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java index 2f74fbf954..c1ea977886 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea608DecoderTest.java @@ -255,6 +255,65 @@ public void onlySelectedChannelIsUsed() throws Exception { assertThat(getOnlyCue(fifthSubtitle).text.toString()).isEqualTo("test subtitle"); } + @Test + public void serviceSwitchOnField1Handled() throws Exception { + Cea608Decoder decoder = + new Cea608Decoder( + MimeTypes.APPLICATION_CEA608, + /* accessibilityChannel= */ 1, // field 1, channel 1 + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); + // field 1 (0xFC header): 'test' then service switch + // field 2 (0xFD header): 'wrong!' + byte[] sample1 = + Bytes.concat( + // 'paint on' control character + createPacket(0xFC, 0x14, 0x29), + createPacket(0xFD, 0x15, 0x29), + createPacket(0xFC, 't', 'e'), + createPacket(0xFD, 'w', 'r'), + createPacket(0xFC, 's', 't'), + createPacket(0xFD, 'o', 'n'), + // Enter TEXT service + createPacket(0xFC, 0x14, 0x2A), + createPacket(0xFD, 'g', '!'), + createPacket(0xFC, 'X', 'X'), + createPacket(0xFD, 0x0, 0x0)); + + Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1)); + + assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test"); + } + + // https://github.com/google/ExoPlayer/issues/10666 + @Test + public void serviceSwitchOnField2Handled() throws Exception { + Cea608Decoder decoder = + new Cea608Decoder( + MimeTypes.APPLICATION_CEA608, + /* accessibilityChannel= */ 3, // field 2, channel 1 + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); + // field 1 (0xFC header): 'wrong!' + // field 2 (0xFD header): 'test' then service switch + byte[] sample1 = + Bytes.concat( + // 'paint on' control character + createPacket(0xFC, 0x14, 0x29), + createPacket(0xFD, 0x15, 0x29), + createPacket(0xFC, 'w', 'r'), + createPacket(0xFD, 't', 'e'), + createPacket(0xFC, 'o', 'n'), + createPacket(0xFD, 's', 't'), + createPacket(0xFC, 'g', '!'), + // Enter TEXT service + createPacket(0xFD, 0x15, 0x2A), + createPacket(0xFC, 0x0, 0x0), + createPacket(0xFD, 'X', 'X')); + + Subtitle firstSubtitle = checkNotNull(decodeSampleAndCopyResult(decoder, sample1)); + + assertThat(getOnlyCue(firstSubtitle).text.toString()).isEqualTo("test"); + } + private static byte[] createPacket(int header, int cc1, int cc2) { return new byte[] { UnsignedBytes.checkedCast(header),